@adonis-agora/media 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +121 -0
- package/README.md +206 -0
- package/dist/providers/media_provider.d.ts +3 -13
- package/dist/providers/media_provider.d.ts.map +1 -1
- package/dist/providers/media_provider.js +151 -4
- package/dist/providers/media_provider.js.map +1 -1
- package/dist/src/content_type.d.ts +6 -1
- package/dist/src/content_type.d.ts.map +1 -1
- package/dist/src/content_type.js +102 -7
- package/dist/src/content_type.js.map +1 -1
- package/dist/src/define_config.d.ts +88 -5
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js +7 -2
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/direct_upload.d.ts +177 -0
- package/dist/src/direct_upload.d.ts.map +1 -0
- package/dist/src/direct_upload.js +299 -0
- package/dist/src/direct_upload.js.map +1 -0
- package/dist/src/direct_upload_handler.d.ts +83 -0
- package/dist/src/direct_upload_handler.d.ts.map +1 -0
- package/dist/src/direct_upload_handler.js +180 -0
- package/dist/src/direct_upload_handler.js.map +1 -0
- package/dist/src/disks/factory.d.ts +11 -3
- package/dist/src/disks/factory.d.ts.map +1 -1
- package/dist/src/disks/factory.js +5 -3
- package/dist/src/disks/factory.js.map +1 -1
- package/dist/src/disks/s3.d.ts +10 -0
- package/dist/src/disks/s3.d.ts.map +1 -1
- package/dist/src/disks/s3.js +96 -22
- package/dist/src/disks/s3.js.map +1 -1
- package/dist/src/disks/sigv4.d.ts +58 -0
- package/dist/src/disks/sigv4.d.ts.map +1 -0
- package/dist/src/disks/sigv4.js +87 -0
- package/dist/src/disks/sigv4.js.map +1 -0
- package/dist/src/disks/xml_fallback.d.ts +3 -3
- package/dist/src/disks/xml_fallback.js +3 -3
- package/dist/src/errors.d.ts +104 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +145 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/hls/delivery.d.ts +112 -0
- package/dist/src/hls/delivery.d.ts.map +1 -0
- package/dist/src/hls/delivery.js +118 -0
- package/dist/src/hls/delivery.js.map +1 -0
- package/dist/src/hls/playlist.d.ts +67 -0
- package/dist/src/hls/playlist.d.ts.map +1 -0
- package/dist/src/hls/playlist.js +126 -0
- package/dist/src/hls/playlist.js.map +1 -0
- package/dist/src/index.d.ts +18 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +13 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/media_collection.d.ts +8 -1
- package/dist/src/media_collection.d.ts.map +1 -1
- package/dist/src/media_collection.js +23 -1
- package/dist/src/media_collection.js.map +1 -1
- package/dist/src/media_library.d.ts +32 -2
- package/dist/src/media_library.d.ts.map +1 -1
- package/dist/src/media_library.js +218 -11
- package/dist/src/media_library.js.map +1 -1
- package/dist/src/media_manager.d.ts +37 -2
- package/dist/src/media_manager.d.ts.map +1 -1
- package/dist/src/media_manager.js +54 -1
- package/dist/src/media_manager.js.map +1 -1
- package/dist/src/media_record.d.ts +22 -2
- package/dist/src/media_record.d.ts.map +1 -1
- package/dist/src/services/booted_app.d.ts +9 -0
- package/dist/src/services/booted_app.d.ts.map +1 -0
- package/dist/src/services/booted_app.js +33 -0
- package/dist/src/services/booted_app.js.map +1 -0
- package/dist/src/services/main.d.ts +12 -0
- package/dist/src/services/main.d.ts.map +1 -1
- package/dist/src/services/main.js +15 -3
- package/dist/src/services/main.js.map +1 -1
- package/dist/src/single_file_store.d.ts +4 -0
- package/dist/src/single_file_store.d.ts.map +1 -1
- package/dist/src/single_file_store.js +8 -4
- package/dist/src/single_file_store.js.map +1 -1
- package/dist/src/testing/fake_transformer.d.ts +32 -0
- package/dist/src/testing/fake_transformer.d.ts.map +1 -0
- package/dist/src/testing/fake_transformer.js +32 -0
- package/dist/src/testing/fake_transformer.js.map +1 -0
- package/dist/src/testing/index.d.ts +2 -0
- package/dist/src/testing/index.d.ts.map +1 -1
- package/dist/src/testing/index.js +1 -0
- package/dist/src/testing/index.js.map +1 -1
- package/dist/src/transformer.d.ts +111 -0
- package/dist/src/transformer.d.ts.map +1 -0
- package/dist/src/transformer.js +2 -0
- package/dist/src/transformer.js.map +1 -0
- package/dist/src/transformers/factory.d.ts +30 -0
- package/dist/src/transformers/factory.d.ts.map +1 -0
- package/dist/src/transformers/factory.js +34 -0
- package/dist/src/transformers/factory.js.map +1 -0
- package/dist/src/transformers/hls.d.ts +110 -0
- package/dist/src/transformers/hls.d.ts.map +1 -0
- package/dist/src/transformers/hls.js +111 -0
- package/dist/src/transformers/hls.js.map +1 -0
- package/dist/src/transformers/mediabunny.d.ts +5 -0
- package/dist/src/transformers/mediabunny.d.ts.map +1 -0
- package/dist/src/transformers/mediabunny.js +150 -0
- package/dist/src/transformers/mediabunny.js.map +1 -0
- package/dist/src/transformers/mediabunny_loader.d.ts +15 -0
- package/dist/src/transformers/mediabunny_loader.d.ts.map +1 -0
- package/dist/src/transformers/mediabunny_loader.js +21 -0
- package/dist/src/transformers/mediabunny_loader.js.map +1 -0
- package/dist/src/transformers/probe.d.ts +56 -0
- package/dist/src/transformers/probe.d.ts.map +1 -0
- package/dist/src/transformers/probe.js +32 -0
- package/dist/src/transformers/probe.js.map +1 -0
- package/dist/src/transformers/source_file.d.ts +20 -0
- package/dist/src/transformers/source_file.d.ts.map +1 -0
- package/dist/src/transformers/source_file.js +30 -0
- package/dist/src/transformers/source_file.js.map +1 -0
- package/dist/stubs/config/media.stub +12 -0
- package/package.json +11 -7
- package/LICENSE +0 -21
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Readable } from 'node:stream';
|
|
2
|
+
import type { MediaRecord } from './media_record.js';
|
|
3
|
+
import type { StorageManager } from './storage_manager.js';
|
|
4
|
+
import type { Disk } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Options for {@link TransformerContext.write}. `contentLength` matters when `contents` is a
|
|
7
|
+
* `Readable` on a disk that streams (S3 needs the length up front); it is ignored for byte payloads.
|
|
8
|
+
*/
|
|
9
|
+
export interface TransformerWriteOptions {
|
|
10
|
+
contentType?: string;
|
|
11
|
+
contentLength?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Everything a {@link Transformer} gets to work with: the record being transformed, read access to
|
|
15
|
+
* its original bytes, and a sandboxed writer for the derived artifacts.
|
|
16
|
+
*
|
|
17
|
+
* The context — not the transformer — decides WHERE artifacts live: every {@link write} lands under
|
|
18
|
+
* {@link outputPrefix} (`<owner>/<id>/conversions/<name>/`), and the library tracks each written
|
|
19
|
+
* path so it can persist the artifact list on the record and delete every artifact when the media
|
|
20
|
+
* is deleted. A transformer never touches disk keys outside its prefix.
|
|
21
|
+
*/
|
|
22
|
+
export interface TransformerContext {
|
|
23
|
+
/** The media record whose content is being transformed. */
|
|
24
|
+
record: MediaRecord;
|
|
25
|
+
/** The disk holding the original object (and receiving the artifacts). */
|
|
26
|
+
disk: Disk;
|
|
27
|
+
/** Name of {@link disk}, as resolvable through the storage manager. */
|
|
28
|
+
diskName: string;
|
|
29
|
+
/**
|
|
30
|
+
* The storage façade, for reads OUTSIDE the record's own disk — auxiliary assets a transformer
|
|
31
|
+
* was configured with (a watermark logo on another disk, a font, a sidecar file). Artifact
|
|
32
|
+
* writes still go through {@link write}, never directly through a disk.
|
|
33
|
+
*/
|
|
34
|
+
storage: StorageManager;
|
|
35
|
+
/** Buffer the original's bytes. Convenient for small originals; prefer {@link getStream} for video. */
|
|
36
|
+
getBytes(): Promise<Uint8Array>;
|
|
37
|
+
/** Stream the original without buffering it. */
|
|
38
|
+
getStream(): Promise<Readable>;
|
|
39
|
+
/**
|
|
40
|
+
* Storage prefix (ending in `/`) reserved for this transformation's artifacts:
|
|
41
|
+
* `<ownerType>/<ownerId>/<collection>/<mediaId>/conversions/<transformerName>/`.
|
|
42
|
+
*/
|
|
43
|
+
outputPrefix: string;
|
|
44
|
+
/**
|
|
45
|
+
* Write one artifact at `outputPrefix + relativePath`. The path is validated (no `..`, no
|
|
46
|
+
* absolute paths) and recorded — the persisted conversion lists every written artifact, which is
|
|
47
|
+
* what makes multi-file outputs (an HLS package) deletable and servable file-by-file.
|
|
48
|
+
*
|
|
49
|
+
* A `Readable` with a known `contentLength` streams straight to the disk; anything else is
|
|
50
|
+
* buffered.
|
|
51
|
+
*/
|
|
52
|
+
write(relativePath: string, contents: Uint8Array | Readable, options?: TransformerWriteOptions): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* What a {@link Transformer.transform} run reports back. The artifacts themselves were already
|
|
56
|
+
* written through {@link TransformerContext.write}; this names which of them is the entry point and
|
|
57
|
+
* attaches free-form metadata.
|
|
58
|
+
*/
|
|
59
|
+
export interface TransformResult {
|
|
60
|
+
/**
|
|
61
|
+
* The entry-point artifact, relative to {@link TransformerContext.outputPrefix} — what
|
|
62
|
+
* `url(id, name)` / `deliver(id, { conversion: name })` resolve for this conversion. Required
|
|
63
|
+
* when any artifact was written (and must be one of them); omit it for a metadata-only
|
|
64
|
+
* transform (a probe, a blurhash) that wrote nothing.
|
|
65
|
+
*/
|
|
66
|
+
entry?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Transformer-specific metadata persisted verbatim on the conversion entry
|
|
69
|
+
* (`record.conversions[name].meta`): duration, resolution, codecs, a blurhash string — whatever
|
|
70
|
+
* the transformer learned. Must be JSON-serializable (the Lucid store serializes it).
|
|
71
|
+
*/
|
|
72
|
+
meta?: Record<string, unknown>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A pluggable content transformation: takes a stored media's content and produces derived
|
|
76
|
+
* artifacts on the disk and/or metadata on the record, persisted as a named conversion
|
|
77
|
+
* (`record.conversions[transformer.name]`).
|
|
78
|
+
*
|
|
79
|
+
* This is the seam the library's own {@link HlsTransformer} and {@link MetadataProbeTransformer}
|
|
80
|
+
* implement, and the one you implement for your own derivations — extract an audio track,
|
|
81
|
+
* stamp a watermark, generate a poster frame, request a transcription. The shapes it supports:
|
|
82
|
+
*
|
|
83
|
+
* - **many artifacts** — an HLS package (segments + playlists); `entry` names the master playlist.
|
|
84
|
+
* - **one artifact** — an optimized image, an extracted audio file; `entry` names it.
|
|
85
|
+
* - **no artifact** — a metadata probe or perceptual hash; return only `meta`.
|
|
86
|
+
* - **auxiliary inputs** — configure them on your transformer instance (bytes, a path, a media id)
|
|
87
|
+
* and read foreign disks through {@link TransformerContext.storage}.
|
|
88
|
+
*
|
|
89
|
+
* Registered per collection (`collections: [{ name, transformers: [...] }]`). Set `eager: true`
|
|
90
|
+
* only for cheap transforms — they run synchronously inside `attach`/`attachExisting` (and so
|
|
91
|
+
* inside a TUS finalize), and a failure rolls the new media back. Heavy transforms (video!) stay
|
|
92
|
+
* deferred: the app triggers them from its own job via `media.library.transform(id, name)`,
|
|
93
|
+
* which is idempotent — already generated ⇒ skip.
|
|
94
|
+
*
|
|
95
|
+
* Import heavy engines lazily INSIDE `transform` (the built-ins import `mediabunny` there), so
|
|
96
|
+
* configuring a transformer never loads its optional peer at boot. A transformer that needs a
|
|
97
|
+
* runtime capability the host lacks (e.g. WebCodecs for re-encoding) should throw
|
|
98
|
+
* {@link TransformerRuntimeMissingError} with a clear hint rather than failing obscurely.
|
|
99
|
+
*/
|
|
100
|
+
export interface Transformer {
|
|
101
|
+
/** Unique conversion name this transformer produces (the key in `record.conversions`). */
|
|
102
|
+
readonly name: string;
|
|
103
|
+
/**
|
|
104
|
+
* Run synchronously on attach (like an eager image conversion) instead of waiting for an
|
|
105
|
+
* explicit `transform()` call. Default `false` — transformations are assumed heavy.
|
|
106
|
+
*/
|
|
107
|
+
readonly eager?: boolean;
|
|
108
|
+
/** Produce the derived artifacts/metadata for one media record. */
|
|
109
|
+
transform(context: TransformerContext): Promise<TransformResult>;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=transformer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transformer.d.ts","sourceRoot":"","sources":["../../src/transformer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEvC;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC,2DAA2D;IAC3D,MAAM,EAAE,WAAW,CAAC;IACpB,0EAA0E;IAC1E,IAAI,EAAE,IAAI,CAAC;IACX,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,OAAO,EAAE,cAAc,CAAC;IACxB,uGAAuG;IACvG,QAAQ,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;IAChC,gDAAgD;IAChD,SAAS,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/B;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,KAAK,CACH,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,UAAU,GAAG,QAAQ,EAC/B,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,WAAW;IAC1B,0FAA0F;IAC1F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,mEAAmE;IACnE,SAAS,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CAClE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transformer.js","sourceRoot":"","sources":["../../src/transformer.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { HlsTransformer, type HlsTransformerOptions } from './hls.js';
|
|
2
|
+
import { MetadataProbeTransformer, type MetadataProbeTransformerOptions } from './probe.js';
|
|
3
|
+
/**
|
|
4
|
+
* The transformer factory namespace used in `config/media.ts` — the {@link Transformer}
|
|
5
|
+
* counterpart of `processors`/`disks`/`stores`:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { defineConfig, transformers } from '@adonis-agora/media'
|
|
9
|
+
*
|
|
10
|
+
* export default defineConfig({
|
|
11
|
+
* collections: [
|
|
12
|
+
* {
|
|
13
|
+
* name: 'videos',
|
|
14
|
+
* acceptsMimeTypes: ['video/mp4'],
|
|
15
|
+
* transformers: [transformers.hls({ targetDuration: 4 }), transformers.probe()],
|
|
16
|
+
* },
|
|
17
|
+
* ],
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Constructing a transformer here never loads its engine — the optional `mediabunny` peer is
|
|
22
|
+
* imported inside the first `transform()` call, so config stays side-effect-free.
|
|
23
|
+
*/
|
|
24
|
+
export declare const transformers: {
|
|
25
|
+
/** Video → HLS package (MPEG-TS segments + playlists) via mediabunny. Default name `'hls'`. */
|
|
26
|
+
hls<Name extends string = "hls">(options?: HlsTransformerOptions<Name>): HlsTransformer<Name>;
|
|
27
|
+
/** Metadata-only probe (duration/resolution/codecs) via mediabunny. Default name `'probe'`. */
|
|
28
|
+
probe<Name extends string = "probe">(options?: MetadataProbeTransformerOptions<Name>): MetadataProbeTransformer<Name>;
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../src/transformers/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,KAAK,+BAA+B,EAAE,MAAM,YAAY,CAAC;AAE5F;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,YAAY;IACvB,+FAA+F;QAC3F,IAAI,SAAS,MAAM,oBAAmB,qBAAqB,CAAC,IAAI,CAAC;IAGrE,+FAA+F;UACzF,IAAI,SAAS,MAAM,sBAAqB,+BAA+B,CAAC,IAAI,CAAC;CAGpF,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { HlsTransformer } from './hls.js';
|
|
2
|
+
import { MetadataProbeTransformer } from './probe.js';
|
|
3
|
+
/**
|
|
4
|
+
* The transformer factory namespace used in `config/media.ts` — the {@link Transformer}
|
|
5
|
+
* counterpart of `processors`/`disks`/`stores`:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { defineConfig, transformers } from '@adonis-agora/media'
|
|
9
|
+
*
|
|
10
|
+
* export default defineConfig({
|
|
11
|
+
* collections: [
|
|
12
|
+
* {
|
|
13
|
+
* name: 'videos',
|
|
14
|
+
* acceptsMimeTypes: ['video/mp4'],
|
|
15
|
+
* transformers: [transformers.hls({ targetDuration: 4 }), transformers.probe()],
|
|
16
|
+
* },
|
|
17
|
+
* ],
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Constructing a transformer here never loads its engine — the optional `mediabunny` peer is
|
|
22
|
+
* imported inside the first `transform()` call, so config stays side-effect-free.
|
|
23
|
+
*/
|
|
24
|
+
export const transformers = {
|
|
25
|
+
/** Video → HLS package (MPEG-TS segments + playlists) via mediabunny. Default name `'hls'`. */
|
|
26
|
+
hls(options = {}) {
|
|
27
|
+
return new HlsTransformer(options);
|
|
28
|
+
},
|
|
29
|
+
/** Metadata-only probe (duration/resolution/codecs) via mediabunny. Default name `'probe'`. */
|
|
30
|
+
probe(options = {}) {
|
|
31
|
+
return new MetadataProbeTransformer(options);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.js","sourceRoot":"","sources":["../../../src/transformers/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAA8B,MAAM,UAAU,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAwC,MAAM,YAAY,CAAC;AAE5F;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,+FAA+F;IAC/F,GAAG,CAA8B,UAAuC,EAAE;QACxE,OAAO,IAAI,cAAc,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC;IACD,+FAA+F;IAC/F,KAAK,CAAgC,UAAiD,EAAE;QACtF,OAAO,IAAI,wBAAwB,CAAC,OAAO,CAAC,CAAC;IAC/C,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { TransformResult, Transformer, TransformerContext } from '../transformer.js';
|
|
2
|
+
/**
|
|
3
|
+
* Lazily supplies a WebCodecs implementation (`{ VideoEncoder, VideoDecoder, AudioEncoder, … }`).
|
|
4
|
+
* Node has no built-in WebCodecs, which is exactly why the HLS transformer is remux-only by
|
|
5
|
+
* default — but NAPI-based implementations exist (e.g. `@napi-rs/webcodecs`), and injecting one
|
|
6
|
+
* unlocks the re-encode fallback for sources whose codecs cannot be stream-copied:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* transformers.hls({ webcodecs: () => import('@napi-rs/webcodecs') })
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* The engine installs the provided classes onto `globalThis` (only the ones missing — a native
|
|
13
|
+
* implementation is never overwritten), because that is where the underlying media engine looks
|
|
14
|
+
* the API up.
|
|
15
|
+
*/
|
|
16
|
+
export type WebCodecsProvider = () => Promise<Record<string, unknown>>;
|
|
17
|
+
/** How a runtime can satisfy the WebCodecs requirement. See {@link resolveWebCodecsSupport}. */
|
|
18
|
+
export type WebCodecsSupport = 'native' | 'injected' | 'absent';
|
|
19
|
+
/**
|
|
20
|
+
* Detect how (whether) WebCodecs is available: `'native'` when the runtime already exposes
|
|
21
|
+
* `VideoEncoder`, `'injected'` when a {@link WebCodecsProvider} supplies one, `'absent'` otherwise.
|
|
22
|
+
* `'absent'` means encoding is impossible — transforms stay stream-copy only, and a transformer
|
|
23
|
+
* that cannot proceed without an encoder should throw `TransformerRuntimeMissingError`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveWebCodecsSupport(provider?: WebCodecsProvider): Promise<WebCodecsSupport>;
|
|
26
|
+
/** What {@link HlsRemuxEngine.remux} receives. */
|
|
27
|
+
export interface HlsRemuxRequest {
|
|
28
|
+
/** Local path of the source file. */
|
|
29
|
+
sourcePath: string;
|
|
30
|
+
/** Local directory the engine writes the whole package into (entry playlist at its root). */
|
|
31
|
+
outputDir: string;
|
|
32
|
+
/** File name of the entry (master) playlist, e.g. `index.m3u8`. */
|
|
33
|
+
entryName: string;
|
|
34
|
+
/** Target (max) segment duration in seconds. */
|
|
35
|
+
targetDuration: number;
|
|
36
|
+
/** Optional WebCodecs implementation enabling the re-encode fallback. */
|
|
37
|
+
webcodecs?: WebCodecsProvider | undefined;
|
|
38
|
+
}
|
|
39
|
+
/** What the engine learned about the source while remuxing — persisted into the conversion `meta`. */
|
|
40
|
+
export interface HlsRemuxSummary {
|
|
41
|
+
durationSeconds?: number;
|
|
42
|
+
width?: number;
|
|
43
|
+
height?: number;
|
|
44
|
+
videoCodec?: string | null;
|
|
45
|
+
audioCodec?: string | null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The engine seam of {@link HlsTransformer}: local file in, HLS package (playlists + segments) in
|
|
49
|
+
* a local directory out. The default engine is mediabunny, loaded lazily on first transform; tests
|
|
50
|
+
* inject a fake that writes files, so every bit of orchestration around the engine is testable
|
|
51
|
+
* without the optional peer.
|
|
52
|
+
*/
|
|
53
|
+
export interface HlsRemuxEngine {
|
|
54
|
+
remux(request: HlsRemuxRequest): Promise<HlsRemuxSummary>;
|
|
55
|
+
}
|
|
56
|
+
export interface HlsTransformerOptions<Name extends string = 'hls'> {
|
|
57
|
+
/** Conversion name on the record. Default `'hls'`. */
|
|
58
|
+
name?: Name;
|
|
59
|
+
/**
|
|
60
|
+
* Run inside attach. Default `false`, and think twice: a remux takes real time, and an eager
|
|
61
|
+
* failure rolls the attach back. The intended flow is deferred —
|
|
62
|
+
* `media.library.transform(id, 'hls')` from a job.
|
|
63
|
+
*/
|
|
64
|
+
eager?: boolean;
|
|
65
|
+
/** Target (max) segment duration in seconds. Default 4. */
|
|
66
|
+
targetDuration?: number;
|
|
67
|
+
/** WebCodecs implementation for the re-encode fallback (see {@link WebCodecsProvider}). */
|
|
68
|
+
webcodecs?: WebCodecsProvider;
|
|
69
|
+
/** Replace the mediabunny engine (mainly for tests). */
|
|
70
|
+
engine?: HlsRemuxEngine;
|
|
71
|
+
}
|
|
72
|
+
/** File name of the entry (master) playlist an {@link HlsTransformer} produces. */
|
|
73
|
+
export declare const HLS_ENTRY_PLAYLIST = "index.m3u8";
|
|
74
|
+
/**
|
|
75
|
+
* The built-in video {@link Transformer}: converts a stored video into an HLS package — MPEG-TS
|
|
76
|
+
* segments plus media playlists behind one master playlist — written under the conversion prefix
|
|
77
|
+
* and persisted as `record.conversions['hls']` with duration/resolution/codec metadata.
|
|
78
|
+
*
|
|
79
|
+
* **Remux-only by default.** The engine (mediabunny, pure JS — no ffmpeg binary) stream-copies
|
|
80
|
+
* the source's h264/aac tracks into segments without re-encoding, because encoding needs
|
|
81
|
+
* WebCodecs, which Node does not provide. Two consequences, both deliberate:
|
|
82
|
+
*
|
|
83
|
+
* - a source whose codecs MPEG-TS cannot carry (vp9, av1…) fails with a clear, definitive
|
|
84
|
+
* `HlsSourceUnsupportedError` — unless a {@link WebCodecsProvider} is configured, which enables
|
|
85
|
+
* the engine's re-encode fallback;
|
|
86
|
+
* - the package has a single variant at the original resolution. Multi-quality renditions are the
|
|
87
|
+
* documented evolution once an encoder is present (mediabunny supports fan-out video options),
|
|
88
|
+
* not something this transformer fakes today.
|
|
89
|
+
*
|
|
90
|
+
* The engine also carries the subtle bit the naive implementation gets wrong: AAC audio with
|
|
91
|
+
* priming (an mp4 edit list) starts at a **negative** timestamp, and a conversion trimmed at t=0
|
|
92
|
+
* would kick that track off the stream-copy fast path — impossible without an encoder. The
|
|
93
|
+
* conversion is therefore trimmed at the source's real first timestamp, shifting all tracks
|
|
94
|
+
* equally (sync is unchanged) and keeping every track copyable.
|
|
95
|
+
*
|
|
96
|
+
* Serving the result is the other half: playlists are stored with storage-relative references and
|
|
97
|
+
* must be rewritten per request — see {@link HlsDeliveryHandler}.
|
|
98
|
+
*/
|
|
99
|
+
export declare class HlsTransformer<Name extends string = 'hls'> implements Transformer {
|
|
100
|
+
readonly name: Name;
|
|
101
|
+
readonly eager: boolean;
|
|
102
|
+
private readonly targetDuration;
|
|
103
|
+
private readonly webcodecs;
|
|
104
|
+
private readonly engine;
|
|
105
|
+
constructor(options?: HlsTransformerOptions<Name>);
|
|
106
|
+
transform(context: TransformerContext): Promise<TransformResult>;
|
|
107
|
+
}
|
|
108
|
+
/** Drop `undefined` values so the persisted meta round-trips identically through JSON stores. */
|
|
109
|
+
export declare function compactMeta(meta: Record<string, unknown>): Record<string, unknown>;
|
|
110
|
+
//# sourceMappingURL=hls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hls.d.ts","sourceRoot":"","sources":["../../../src/transformers/hls.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAI1F;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAEvE,gGAAgG;AAChG,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEhE;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,CAAC,EAAE,iBAAiB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAI3B;AAED,kDAAkD;AAClD,MAAM,WAAW,eAAe;IAC9B,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,6FAA6F;IAC7F,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,cAAc,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,SAAS,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;CAC3C;AAED,sGAAsG;AACtG,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,qBAAqB,CAAC,IAAI,SAAS,MAAM,GAAG,KAAK;IAChE,sDAAsD;IACtD,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2FAA2F;IAC3F,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,wDAAwD;IACxD,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED,mFAAmF;AACnF,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,cAAc,CAAC,IAAI,SAAS,MAAM,GAAG,KAAK,CAAE,YAAW,WAAW;IAC7E,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;IAC1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;gBAExC,OAAO,GAAE,qBAAqB,CAAC,IAAI,CAAM;IAQ/C,SAAS,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,CAAC;CA8CvE;AAED,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAElF"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
3
|
+
import { mkdir } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { hlsArtifactContentType } from '../hls/playlist.js';
|
|
6
|
+
import { loadMediabunny } from './mediabunny_loader.js';
|
|
7
|
+
import { withSourceFile } from './source_file.js';
|
|
8
|
+
/**
|
|
9
|
+
* Detect how (whether) WebCodecs is available: `'native'` when the runtime already exposes
|
|
10
|
+
* `VideoEncoder`, `'injected'` when a {@link WebCodecsProvider} supplies one, `'absent'` otherwise.
|
|
11
|
+
* `'absent'` means encoding is impossible — transforms stay stream-copy only, and a transformer
|
|
12
|
+
* that cannot proceed without an encoder should throw `TransformerRuntimeMissingError`.
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveWebCodecsSupport(provider) {
|
|
15
|
+
if (globalThis.VideoEncoder !== undefined)
|
|
16
|
+
return 'native';
|
|
17
|
+
if (provider && (await provider()).VideoEncoder !== undefined)
|
|
18
|
+
return 'injected';
|
|
19
|
+
return 'absent';
|
|
20
|
+
}
|
|
21
|
+
/** File name of the entry (master) playlist an {@link HlsTransformer} produces. */
|
|
22
|
+
export const HLS_ENTRY_PLAYLIST = 'index.m3u8';
|
|
23
|
+
/**
|
|
24
|
+
* The built-in video {@link Transformer}: converts a stored video into an HLS package — MPEG-TS
|
|
25
|
+
* segments plus media playlists behind one master playlist — written under the conversion prefix
|
|
26
|
+
* and persisted as `record.conversions['hls']` with duration/resolution/codec metadata.
|
|
27
|
+
*
|
|
28
|
+
* **Remux-only by default.** The engine (mediabunny, pure JS — no ffmpeg binary) stream-copies
|
|
29
|
+
* the source's h264/aac tracks into segments without re-encoding, because encoding needs
|
|
30
|
+
* WebCodecs, which Node does not provide. Two consequences, both deliberate:
|
|
31
|
+
*
|
|
32
|
+
* - a source whose codecs MPEG-TS cannot carry (vp9, av1…) fails with a clear, definitive
|
|
33
|
+
* `HlsSourceUnsupportedError` — unless a {@link WebCodecsProvider} is configured, which enables
|
|
34
|
+
* the engine's re-encode fallback;
|
|
35
|
+
* - the package has a single variant at the original resolution. Multi-quality renditions are the
|
|
36
|
+
* documented evolution once an encoder is present (mediabunny supports fan-out video options),
|
|
37
|
+
* not something this transformer fakes today.
|
|
38
|
+
*
|
|
39
|
+
* The engine also carries the subtle bit the naive implementation gets wrong: AAC audio with
|
|
40
|
+
* priming (an mp4 edit list) starts at a **negative** timestamp, and a conversion trimmed at t=0
|
|
41
|
+
* would kick that track off the stream-copy fast path — impossible without an encoder. The
|
|
42
|
+
* conversion is therefore trimmed at the source's real first timestamp, shifting all tracks
|
|
43
|
+
* equally (sync is unchanged) and keeping every track copyable.
|
|
44
|
+
*
|
|
45
|
+
* Serving the result is the other half: playlists are stored with storage-relative references and
|
|
46
|
+
* must be rewritten per request — see {@link HlsDeliveryHandler}.
|
|
47
|
+
*/
|
|
48
|
+
export class HlsTransformer {
|
|
49
|
+
name;
|
|
50
|
+
eager;
|
|
51
|
+
targetDuration;
|
|
52
|
+
webcodecs;
|
|
53
|
+
engine;
|
|
54
|
+
constructor(options = {}) {
|
|
55
|
+
this.name = options.name ?? 'hls';
|
|
56
|
+
this.eager = options.eager ?? false;
|
|
57
|
+
this.targetDuration = options.targetDuration ?? 4;
|
|
58
|
+
this.webcodecs = options.webcodecs;
|
|
59
|
+
this.engine = options.engine;
|
|
60
|
+
}
|
|
61
|
+
async transform(context) {
|
|
62
|
+
const engine = this.engine ?? (await loadMediabunny(this.name)).createHlsEngine();
|
|
63
|
+
return withSourceFile(context, async ({ sourcePath, tempDir }) => {
|
|
64
|
+
const outputDir = join(tempDir, 'hls');
|
|
65
|
+
await mkdir(outputDir, { recursive: true });
|
|
66
|
+
const summary = await engine.remux({
|
|
67
|
+
sourcePath,
|
|
68
|
+
outputDir,
|
|
69
|
+
entryName: HLS_ENTRY_PLAYLIST,
|
|
70
|
+
targetDuration: this.targetDuration,
|
|
71
|
+
webcodecs: this.webcodecs,
|
|
72
|
+
});
|
|
73
|
+
// Upload the whole package the engine wrote — playlists and segments alike — streaming each
|
|
74
|
+
// file (a segment never has to fit in memory). Sorted for deterministic order in
|
|
75
|
+
// `conversions[name].files`.
|
|
76
|
+
const files = (await readdir(outputDir, { recursive: true })).sort();
|
|
77
|
+
let segmentCount = 0;
|
|
78
|
+
let playlistCount = 0;
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
const relative = file.split('\\').join('/');
|
|
81
|
+
const local = join(outputDir, file);
|
|
82
|
+
const info = await stat(local);
|
|
83
|
+
if (!info.isFile())
|
|
84
|
+
continue;
|
|
85
|
+
if (relative.toLowerCase().endsWith('.m3u8'))
|
|
86
|
+
playlistCount += 1;
|
|
87
|
+
else
|
|
88
|
+
segmentCount += 1;
|
|
89
|
+
const contentType = hlsArtifactContentType(relative);
|
|
90
|
+
await context.write(relative, createReadStream(local), {
|
|
91
|
+
contentLength: info.size,
|
|
92
|
+
...(contentType !== undefined ? { contentType } : {}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
entry: HLS_ENTRY_PLAYLIST,
|
|
97
|
+
meta: compactMeta({
|
|
98
|
+
...summary,
|
|
99
|
+
segmentCount,
|
|
100
|
+
playlistCount,
|
|
101
|
+
targetDuration: this.targetDuration,
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Drop `undefined` values so the persisted meta round-trips identically through JSON stores. */
|
|
108
|
+
export function compactMeta(meta) {
|
|
109
|
+
return Object.fromEntries(Object.entries(meta).filter(([, value]) => value !== undefined));
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=hls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hls.js","sourceRoot":"","sources":["../../../src/transformers/hls.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mediabunny.d.ts","sourceRoot":"","sources":["../../../src/transformers/mediabunny.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAuD,MAAM,UAAU,CAAC;AACpG,OAAO,KAAK,EAAE,gBAAgB,EAAqB,MAAM,YAAY,CAAC;AA+DtE,wBAAgB,eAAe,IAAI,cAAc,CA8DhD;AAED,wBAAgB,iBAAiB,IAAI,gBAAgB,CAiCpD"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { ALL_FORMATS, Conversion, FilePathSource, FilePathTarget, HlsOutputFormat, Input, MpegTsOutputFormat, Output, PathedTarget, } from 'mediabunny';
|
|
3
|
+
import { HlsSourceUnsupportedError } from '../errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* The mediabunny-backed engines — the ONLY module in the package that imports the optional
|
|
6
|
+
* `mediabunny` peer, reached lazily through `mediabunny_loader.ts` at first transform.
|
|
7
|
+
*
|
|
8
|
+
* Why mediabunny: pure TypeScript, no ffmpeg binary — a production container without system
|
|
9
|
+
* ffmpeg (the usual case) remuxes fine. The trade-off is that *encoding* rides on WebCodecs,
|
|
10
|
+
* which Node lacks, hence the remux-only default and the {@link WebCodecsProvider} injection
|
|
11
|
+
* seam documented on `HlsTransformer`.
|
|
12
|
+
*/
|
|
13
|
+
/** The WebCodecs API surface mediabunny reads from `globalThis`. */
|
|
14
|
+
const WEBCODECS_GLOBALS = [
|
|
15
|
+
'VideoEncoder',
|
|
16
|
+
'VideoDecoder',
|
|
17
|
+
'AudioEncoder',
|
|
18
|
+
'AudioDecoder',
|
|
19
|
+
'VideoFrame',
|
|
20
|
+
'AudioData',
|
|
21
|
+
'EncodedVideoChunk',
|
|
22
|
+
'EncodedAudioChunk',
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Install an injected WebCodecs implementation onto `globalThis` — only the classes that are
|
|
26
|
+
* missing; a native implementation is never overwritten. Global mutation is deliberate and
|
|
27
|
+
* additive: `globalThis` is where the WebCodecs spec (and therefore mediabunny) looks the API up,
|
|
28
|
+
* so "injecting an implementation" and "installing it globally" are the same thing. Installing
|
|
29
|
+
* per-conversion and uninstalling after would race concurrent transforms for zero benefit.
|
|
30
|
+
*/
|
|
31
|
+
async function installWebCodecs(provider) {
|
|
32
|
+
if (!provider)
|
|
33
|
+
return;
|
|
34
|
+
const implementation = await provider();
|
|
35
|
+
const globals = globalThis;
|
|
36
|
+
for (const name of WEBCODECS_GLOBALS) {
|
|
37
|
+
if (globals[name] === undefined && implementation[name] !== undefined) {
|
|
38
|
+
globals[name] = implementation[name];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Best-effort technical summary of an input — never fails the transform over metadata. */
|
|
43
|
+
async function summarize(input) {
|
|
44
|
+
const summary = {};
|
|
45
|
+
try {
|
|
46
|
+
summary.durationSeconds = await input.computeDuration();
|
|
47
|
+
const video = await input.getPrimaryVideoTrack();
|
|
48
|
+
if (video) {
|
|
49
|
+
summary.width = await video.getDisplayWidth();
|
|
50
|
+
summary.height = await video.getDisplayHeight();
|
|
51
|
+
summary.videoCodec = await video.getCodec();
|
|
52
|
+
}
|
|
53
|
+
const audio = await input.getPrimaryAudioTrack();
|
|
54
|
+
if (audio) {
|
|
55
|
+
summary.audioCodec = await audio.getCodec();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// metadata is a bonus; the artifacts are the product
|
|
60
|
+
}
|
|
61
|
+
return summary;
|
|
62
|
+
}
|
|
63
|
+
export function createHlsEngine() {
|
|
64
|
+
return {
|
|
65
|
+
async remux(request) {
|
|
66
|
+
await installWebCodecs(request.webcodecs);
|
|
67
|
+
const input = new Input({
|
|
68
|
+
source: new FilePathSource(request.sourcePath),
|
|
69
|
+
formats: ALL_FORMATS,
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
const output = new Output({
|
|
73
|
+
format: new HlsOutputFormat({
|
|
74
|
+
segmentFormat: new MpegTsOutputFormat(),
|
|
75
|
+
targetDuration: request.targetDuration,
|
|
76
|
+
}),
|
|
77
|
+
// The HLS output is many files; each one lands in the local output directory.
|
|
78
|
+
target: new PathedTarget(join(request.outputDir, request.entryName), (target) => new FilePathTarget(target.path)),
|
|
79
|
+
});
|
|
80
|
+
// THE gotcha: AAC audio with priming (an mp4 edit list) starts at a NEGATIVE timestamp.
|
|
81
|
+
// Mediabunny's default trims the conversion at t=0, which would throw that audio track off
|
|
82
|
+
// the packet-copy fast path — and re-encoding it needs an encoder we may not have.
|
|
83
|
+
// Trimming at the source's real first timestamp keeps every track a pure stream copy; all
|
|
84
|
+
// tracks shift equally, so A/V sync is untouched.
|
|
85
|
+
const firstTimestamp = await input.getFirstTimestamp();
|
|
86
|
+
const conversion = await Conversion.init({
|
|
87
|
+
input,
|
|
88
|
+
output,
|
|
89
|
+
trim: { start: firstTimestamp },
|
|
90
|
+
});
|
|
91
|
+
// With no video/audio options, mediabunny copies compatible tracks and re-encodes the
|
|
92
|
+
// rest — but re-encoding needs an encoder, so without WebCodecs an incompatible track is
|
|
93
|
+
// DISCARDED instead. Silently shipping a video without its audio (or vice versa) is not a
|
|
94
|
+
// conversion, it is data loss: fail loudly, with the codec and the reason per track.
|
|
95
|
+
if (conversion.discardedTracks.length > 0) {
|
|
96
|
+
const reasons = await Promise.all(conversion.discardedTracks.map(async (discarded) => {
|
|
97
|
+
const codec = await discarded.track.getCodec().catch(() => null);
|
|
98
|
+
return `${discarded.track.type}/${codec ?? 'unknown'}: ${discarded.reason}`;
|
|
99
|
+
}));
|
|
100
|
+
throw new HlsSourceUnsupportedError(reasons.join('; '));
|
|
101
|
+
}
|
|
102
|
+
if (!conversion.isValid) {
|
|
103
|
+
throw new HlsSourceUnsupportedError('the file contains no usable track');
|
|
104
|
+
}
|
|
105
|
+
const summary = await summarize(input);
|
|
106
|
+
await conversion.execute();
|
|
107
|
+
return summary;
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
input.dispose();
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export function createProbeEngine() {
|
|
116
|
+
return {
|
|
117
|
+
async probe(sourcePath) {
|
|
118
|
+
const input = new Input({ source: new FilePathSource(sourcePath), formats: ALL_FORMATS });
|
|
119
|
+
try {
|
|
120
|
+
const format = await input.getFormat();
|
|
121
|
+
const summary = {
|
|
122
|
+
format: format.name,
|
|
123
|
+
mimeType: await input.getMimeType(),
|
|
124
|
+
durationSeconds: await input.computeDuration(),
|
|
125
|
+
hasVideo: false,
|
|
126
|
+
hasAudio: false,
|
|
127
|
+
};
|
|
128
|
+
const video = await input.getPrimaryVideoTrack();
|
|
129
|
+
if (video) {
|
|
130
|
+
summary.hasVideo = true;
|
|
131
|
+
summary.width = await video.getDisplayWidth();
|
|
132
|
+
summary.height = await video.getDisplayHeight();
|
|
133
|
+
summary.videoCodec = await video.getCodec();
|
|
134
|
+
}
|
|
135
|
+
const audio = await input.getPrimaryAudioTrack();
|
|
136
|
+
if (audio) {
|
|
137
|
+
summary.hasAudio = true;
|
|
138
|
+
summary.audioCodec = await audio.getCodec();
|
|
139
|
+
summary.audioSampleRate = await audio.getSampleRate();
|
|
140
|
+
summary.audioChannels = await audio.getNumberOfChannels();
|
|
141
|
+
}
|
|
142
|
+
return summary;
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
input.dispose();
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=mediabunny.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mediabunny.js","sourceRoot":"","sources":["../../../src/transformers/mediabunny.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HlsRemuxEngine } from './hls.js';
|
|
2
|
+
import type { MediaProbeEngine } from './probe.js';
|
|
3
|
+
/** The engine factories `./mediabunny.js` exports (the only module that imports the peer). */
|
|
4
|
+
export interface MediabunnyEngineModule {
|
|
5
|
+
createHlsEngine(): HlsRemuxEngine;
|
|
6
|
+
createProbeEngine(): MediaProbeEngine;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Load the mediabunny-backed engine module, translating a missing optional peer into the typed
|
|
10
|
+
* {@link TransformerRuntimeMissingError} instead of a raw `ERR_MODULE_NOT_FOUND` five imports deep.
|
|
11
|
+
* Import happens here — at first transform — never at boot: configuring a transformer must not
|
|
12
|
+
* load its engine (the same laziness `processors.sharp()` and `disks.s3()` follow).
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadMediabunny(transformerName: string): Promise<MediabunnyEngineModule>;
|
|
15
|
+
//# sourceMappingURL=mediabunny_loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mediabunny_loader.d.ts","sourceRoot":"","sources":["../../../src/transformers/mediabunny_loader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,8FAA8F;AAC9F,MAAM,WAAW,sBAAsB;IACrC,eAAe,IAAI,cAAc,CAAC;IAClC,iBAAiB,IAAI,gBAAgB,CAAC;CACvC;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAiB7F"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { TransformerRuntimeMissingError } from '../errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Load the mediabunny-backed engine module, translating a missing optional peer into the typed
|
|
4
|
+
* {@link TransformerRuntimeMissingError} instead of a raw `ERR_MODULE_NOT_FOUND` five imports deep.
|
|
5
|
+
* Import happens here — at first transform — never at boot: configuring a transformer must not
|
|
6
|
+
* load its engine (the same laziness `processors.sharp()` and `disks.s3()` follow).
|
|
7
|
+
*/
|
|
8
|
+
export async function loadMediabunny(transformerName) {
|
|
9
|
+
try {
|
|
10
|
+
return await import('./mediabunny.js');
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND' &&
|
|
14
|
+
error instanceof Error &&
|
|
15
|
+
error.message.includes('mediabunny')) {
|
|
16
|
+
throw new TransformerRuntimeMissingError(transformerName, "the optional peer dependency 'mediabunny'", 'Install it (`pnpm add mediabunny`) or inject a custom `engine`.');
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=mediabunny_loader.js.map
|