@driftengine/media 3.61.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.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * What this browser will encode, asked before anything is built.
3
+ *
4
+ * One responsibility: turn a frame size into the codec strings an encode is negotiated with, and
5
+ * answer whether this browser accepts them. `ClipEncoder.open` configures from the same ladder,
6
+ * so a caller greying out a control and a caller pressing Export are asking one question rather
7
+ * than two copies of it.
8
+ *
9
+ * **That was not the arrangement, and the gap shipped.** Both rungs used to be level 4.0 written
10
+ * into the string by hand, and a level is a promise about frame *size*: 4.0 holds 8192
11
+ * macroblocks, which is 1920x1080 with 32 to spare and nothing above it. So `isConfigSupported`
12
+ * refused 2560x1440, 2160x2160 and 3840x2160 outright, `VideoEncoder.configure` was never reached
13
+ * at those sizes, and a 4K export ended in "this browser cannot encode video at that size" on a
14
+ * card that encodes 4K. A second consumer found it by copying the two strings into its own
15
+ * repository to grey out a panel with, which is the shape of a decision that has no home.
16
+ */
17
+ /**
18
+ * Whether this browser can render a clip offline rather than record one.
19
+ *
20
+ * Both halves are needed: frames without a score is a silent clip and a score without
21
+ * frames is nothing. A browser missing either falls back to the recorder.
22
+ *
23
+ * Says nothing about a *size*, deliberately: it is about the API being present at all, and it
24
+ * answers the same for a 64x64 frame and a 4K one. `clipEncodingSupported` is the size question.
25
+ */
26
+ export declare function offlineEncodingSupported(): boolean;
27
+ /**
28
+ * The four numbers an encode is negotiated with, before there is anything to encode.
29
+ *
30
+ * `ClipEncoderOptions` extends it, so the question a gate asks is the options the export opens
31
+ * with rather than a paraphrase of them.
32
+ */
33
+ export interface ClipEncodeRequest {
34
+ readonly width: number;
35
+ readonly height: number;
36
+ readonly fps: number;
37
+ /** Bits per second for video. See `clipBitrate` for a figure worth passing. */
38
+ readonly bitrate: number;
39
+ }
40
+ /**
41
+ * The codec strings to try for a frame this size, best first.
42
+ *
43
+ * High profile first, constrained baseline as the fallback, both at the smallest level that can
44
+ * represent the frame. High is what every phone decodes in hardware and what a transcoder
45
+ * prefers; baseline exists because a machine without hardware AVC will refuse the first and
46
+ * encode the second in software rather than refuse to export. **Both rungs move together**: a
47
+ * baseline rung pinned to 4.0 is no fallback at 4K, it is a second refusal.
48
+ *
49
+ * Empty when no level can hold the frame, which is a frame past 139,264 macroblocks. The caller
50
+ * turns that into the same refusal it gives when the browser says no, one round trip earlier.
51
+ *
52
+ * Not on the barrel on purpose. The ladder is the engine's decision, and a consumer that can read
53
+ * it is a consumer that can pin a codec string in its own repository, which is how one decision
54
+ * became two copies the last time.
55
+ */
56
+ export declare function clipCodecs(width: number, height: number): readonly string[];
57
+ /**
58
+ * Whether this browser will encode a clip of this size, at this rate, at this bitrate.
59
+ *
60
+ * The question `ClipEncoder.open` is about to ask, asked early enough to grey out a control
61
+ * instead of failing an export somebody already chose. It walks the same ladder `open` walks, so
62
+ * the two cannot drift; a gate built from a copied codec string offered ten sizes the export
63
+ * would refuse, which is the fault this exists to make unrepeatable.
64
+ *
65
+ * Memoised on the whole request, because the answer is a property of the machine rather than of
66
+ * the moment: a panel asking about nine sizes at two rates costs eighteen probes at boot and
67
+ * nothing afterwards. The promise is stored rather than the result, so eighteen concurrent asks
68
+ * about one size are one probe.
69
+ *
70
+ * False when WebCodecs is absent, which is the same answer from the caller's point of view. What
71
+ * it does **not** answer: whether the frames arrive once encoding starts. See `framesReachEncoder`.
72
+ */
73
+ export declare function clipEncodingSupported(request: ClipEncodeRequest): Promise<boolean>;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * What this browser will encode, asked before anything is built.
3
+ *
4
+ * One responsibility: turn a frame size into the codec strings an encode is negotiated with, and
5
+ * answer whether this browser accepts them. `ClipEncoder.open` configures from the same ladder,
6
+ * so a caller greying out a control and a caller pressing Export are asking one question rather
7
+ * than two copies of it.
8
+ *
9
+ * **That was not the arrangement, and the gap shipped.** Both rungs used to be level 4.0 written
10
+ * into the string by hand, and a level is a promise about frame *size*: 4.0 holds 8192
11
+ * macroblocks, which is 1920x1080 with 32 to spare and nothing above it. So `isConfigSupported`
12
+ * refused 2560x1440, 2160x2160 and 3840x2160 outright, `VideoEncoder.configure` was never reached
13
+ * at those sizes, and a 4K export ended in "this browser cannot encode video at that size" on a
14
+ * card that encodes 4K. A second consumer found it by copying the two strings into its own
15
+ * repository to grey out a panel with, which is the shape of a decision that has no home.
16
+ */
17
+ /**
18
+ * Whether this browser can render a clip offline rather than record one.
19
+ *
20
+ * Both halves are needed: frames without a score is a silent clip and a score without
21
+ * frames is nothing. A browser missing either falls back to the recorder.
22
+ *
23
+ * Says nothing about a *size*, deliberately: it is about the API being present at all, and it
24
+ * answers the same for a 64x64 frame and a 4K one. `clipEncodingSupported` is the size question.
25
+ */
26
+ export function offlineEncodingSupported() {
27
+ return (typeof VideoEncoder === 'function' &&
28
+ typeof AudioEncoder === 'function' &&
29
+ typeof VideoFrame === 'function' &&
30
+ typeof AudioData === 'function' &&
31
+ typeof OfflineAudioContext === 'function');
32
+ }
33
+ /**
34
+ * H.264 levels, smallest first, with the largest frame each can represent.
35
+ *
36
+ * `maxMacroblocks` is the spec's MaxFS, and on Chrome 149 it is exactly what
37
+ * `isConfigSupported` enforces: measured over 9 sizes, 2 rates and 7 levels on an RX 9070 XT,
38
+ * every level at or above the one whose MaxFS holds the frame answered yes and every level below
39
+ * it answered no, in all 252 cells. Profile never moved an answer and neither did frame rate.
40
+ *
41
+ * **The rate bound is deliberately not part of the choice**, and that is the compromise here.
42
+ * The spec's MaxMBPS says 1920x1080 at 60 needs level 4.2 and at 120 needs 5.1; this asks 4.0 for
43
+ * both. It is defensible because the level in a codec string is a floor for the negotiation
44
+ * rather than a description of the file: asked for 4.0 at 1920x1080, Chrome wrote **level 4.2**
45
+ * into the file, and asked for 5.1 at 3840x2160 it wrote 5.2. What would make it wrong is a
46
+ * browser that honours the floor literally and encodes a stream its own level cannot carry, which
47
+ * nothing here has met. The rungs are worth having anyway for the size wall, which is real.
48
+ *
49
+ * Levels that hold no more than the one below are left out rather than listed: 4.1 has 4.0's
50
+ * 8192, 5.2 has 5.1's 36864, 6.1 and 6.2 have 6.0's 139264. Asking for one of them buys a bigger
51
+ * number in the string and no frame. Measured, at 3840x2160: a 5.1 request and a 5.2 request
52
+ * produced the same byte count and the same level in the file.
53
+ *
54
+ * The table starts at 4.0 rather than at 1.0 because nothing below it changes an answer. A 720p
55
+ * fallback frame is 3600 macroblocks and level 4.0 carries it, the encoder writes its own level
56
+ * regardless, and lowering the floor would only put a smaller number in front of a browser that
57
+ * has never been measured caring.
58
+ */
59
+ const AVC_LEVELS = [
60
+ { id: 0x28, maxMacroblocks: 8192 }, // 4.0
61
+ { id: 0x2a, maxMacroblocks: 8704 }, // 4.2
62
+ { id: 0x32, maxMacroblocks: 22_080 }, // 5.0
63
+ { id: 0x33, maxMacroblocks: 36_864 }, // 5.1
64
+ { id: 0x3c, maxMacroblocks: 139_264 }, // 6.0
65
+ ];
66
+ /**
67
+ * The codec strings to try for a frame this size, best first.
68
+ *
69
+ * High profile first, constrained baseline as the fallback, both at the smallest level that can
70
+ * represent the frame. High is what every phone decodes in hardware and what a transcoder
71
+ * prefers; baseline exists because a machine without hardware AVC will refuse the first and
72
+ * encode the second in software rather than refuse to export. **Both rungs move together**: a
73
+ * baseline rung pinned to 4.0 is no fallback at 4K, it is a second refusal.
74
+ *
75
+ * Empty when no level can hold the frame, which is a frame past 139,264 macroblocks. The caller
76
+ * turns that into the same refusal it gives when the browser says no, one round trip earlier.
77
+ *
78
+ * Not on the barrel on purpose. The ladder is the engine's decision, and a consumer that can read
79
+ * it is a consumer that can pin a codec string in its own repository, which is how one decision
80
+ * became two copies the last time.
81
+ */
82
+ export function clipCodecs(width, height) {
83
+ /* Whole macroblocks: 1080 is 67.5 columns of 16 and costs 68 of them. */
84
+ const macroblocks = Math.ceil(width / 16) * Math.ceil(height / 16);
85
+ const level = AVC_LEVELS.find((rung) => macroblocks <= rung.maxMacroblocks);
86
+ if (level === undefined)
87
+ return [];
88
+ const hex = level.id.toString(16).toUpperCase().padStart(2, '0');
89
+ return [`avc1.6400${hex}`, `avc1.42E0${hex}`];
90
+ }
91
+ /** One answer per distinct request, kept for the session. See `clipEncodingSupported`. */
92
+ const answers = new Map();
93
+ /**
94
+ * Whether this browser will encode a clip of this size, at this rate, at this bitrate.
95
+ *
96
+ * The question `ClipEncoder.open` is about to ask, asked early enough to grey out a control
97
+ * instead of failing an export somebody already chose. It walks the same ladder `open` walks, so
98
+ * the two cannot drift; a gate built from a copied codec string offered ten sizes the export
99
+ * would refuse, which is the fault this exists to make unrepeatable.
100
+ *
101
+ * Memoised on the whole request, because the answer is a property of the machine rather than of
102
+ * the moment: a panel asking about nine sizes at two rates costs eighteen probes at boot and
103
+ * nothing afterwards. The promise is stored rather than the result, so eighteen concurrent asks
104
+ * about one size are one probe.
105
+ *
106
+ * False when WebCodecs is absent, which is the same answer from the caller's point of view. What
107
+ * it does **not** answer: whether the frames arrive once encoding starts. See `framesReachEncoder`.
108
+ */
109
+ export async function clipEncodingSupported(request) {
110
+ const { width, height, fps, bitrate } = request;
111
+ const key = `${width}x${height}@${fps}/${bitrate}`;
112
+ const known = answers.get(key);
113
+ if (known !== undefined)
114
+ return known;
115
+ const asking = ask(request);
116
+ answers.set(key, asking);
117
+ return asking;
118
+ }
119
+ async function ask({ width, height, fps, bitrate }) {
120
+ if (typeof VideoEncoder !== 'function')
121
+ return false;
122
+ for (const codec of clipCodecs(width, height)) {
123
+ try {
124
+ const support = await VideoEncoder.isConfigSupported({
125
+ codec,
126
+ width,
127
+ height,
128
+ bitrate,
129
+ framerate: fps,
130
+ });
131
+ if (support.supported === true)
132
+ return true;
133
+ }
134
+ catch {
135
+ // A codec string this build will not parse is one it cannot encode. Try the next rung.
136
+ }
137
+ }
138
+ return false;
139
+ }
@@ -0,0 +1,24 @@
1
+ /** What the probe found. `unknown` is the answer that changes nothing. */
2
+ export type FrameDelivery = 'carries' | 'empty' | 'unknown';
3
+ /**
4
+ * What a decoded probe colour means.
5
+ *
6
+ * **Only a positive reading of an empty frame refuses anything.** A probe that could not run, a
7
+ * decoder that returned nothing, a colour this does not recognise: all `unknown`, and a caller
8
+ * lets the export through on all of them. The two mistakes are not equal. A false negative costs
9
+ * somebody a file they can already see is wrong; a false positive costs them every clip they were
10
+ * going to make, on a machine where nothing was ever broken.
11
+ */
12
+ export declare function deliveryVerdict(decoded: readonly [number, number, number] | null): FrameDelivery;
13
+ /**
14
+ * Paint a known colour, encode it, decode it back, and report what came out.
15
+ *
16
+ * Memoised for the session: the fault is a property of how the browser was started, so a second
17
+ * answer cannot differ from the first, and an export path that asks before every attempt should
18
+ * pay for one encode rather than one per press.
19
+ *
20
+ * Every failure path answers `unknown` rather than throwing, including the timeout. This runs in
21
+ * front of an export, and a capability probe that can itself stop one has become a liability
22
+ * rather than a check.
23
+ */
24
+ export declare function framesReachEncoder(): Promise<FrameDelivery>;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Whether the pictures this browser encodes are the pictures it was given.
3
+ *
4
+ * Every other capability call asks whether the browser *says* it can do something. This one asks
5
+ * whether it did, and it is the only one that has caught a file going out wrong.
6
+ *
7
+ * **The fault it exists for.** Chrome 149 with `chrome://flags#enable-vulkan` on, under Wayland,
8
+ * composites through Vulkan while ANGLE stays on GL, and in that arrangement a `VideoFrame` built
9
+ * from a canvas reaches the encoder empty. Nothing reports it: `VideoEncoder.error` never fires,
10
+ * `encodeQueueSize` drains, `flush` resolves, the muxer finalises, and the app hands somebody a
11
+ * clip that is solid green from end to end with its audio intact. Four went out that way from a
12
+ * consuming app on 2026-08-14 before anybody opened one.
13
+ *
14
+ * Reproduced here from this repository's own harness, on an RX 9070 XT on RADV, with the flags as
15
+ * the only variable: twelve frames of 1920x1080 encoded to 114,157 bytes with them off and 8,747
16
+ * bytes with them on, and `ffprobe` read every frame of the second file back as YAVG 0, UAVG 0,
17
+ * VAVG 0. All-zero YUV is green rather than black once a decoder applies the limited range.
18
+ * `--headless=new` does not reproduce it at all, with or without Vulkan, which is consistent with
19
+ * it taking the compositor and the ozone platform together.
20
+ *
21
+ * **So the check is a round trip rather than an inspection**, and three things that look like
22
+ * witnesses are not: `getImageData` on the same canvas returns the right pixels in every run,
23
+ * `VideoFrame.copyTo` reported full buffers for a case that went on to encode green, and every
24
+ * capability call answers yes. What would make this module wrong is a browser that fails delivery
25
+ * for some frames and not others, in which case one probe at the start is not enough; the fault
26
+ * measured here is a property of the browser's configuration and does not vary frame to frame.
27
+ */
28
+ import { clipCodecs } from './clipSupport.js';
29
+ /** Side of the probe frame. The smallest thing worth asking; it encodes to 39 bytes. */
30
+ const PROBE_SIZE = 64;
31
+ /**
32
+ * The colour the probe paints: full red and full blue, no green.
33
+ *
34
+ * Chosen to be the opposite of the failure. An all-zero frame decodes to green with no red and no
35
+ * blue in either colour space a browser might tag it with, BT.709 putting it at `0,77,0` and
36
+ * BT.601 at `0,135,0`, so a picture that survived and a picture that did not cannot be confused
37
+ * by a decoder's choice of matrix. A grey probe would have to tell them apart by brightness,
38
+ * which is a threshold somebody has to guess at.
39
+ */
40
+ const PROBE_COLOR = 'rgb(255 0 255)';
41
+ /** How long the round trip gets before it is abandoned. Far longer than it has ever taken. */
42
+ const PROBE_TIMEOUT_MS = 2000;
43
+ /**
44
+ * What a decoded probe colour means.
45
+ *
46
+ * **Only a positive reading of an empty frame refuses anything.** A probe that could not run, a
47
+ * decoder that returned nothing, a colour this does not recognise: all `unknown`, and a caller
48
+ * lets the export through on all of them. The two mistakes are not equal. A false negative costs
49
+ * somebody a file they can already see is wrong; a false positive costs them every clip they were
50
+ * going to make, on a machine where nothing was ever broken.
51
+ */
52
+ export function deliveryVerdict(decoded) {
53
+ if (decoded === null)
54
+ return 'unknown';
55
+ const [red, , blue] = decoded;
56
+ /* Both channels the probe painted came back. Generous, because the round trip goes through a
57
+ 4:2:0 chroma subsample and a colour space conversion and neither is lossless. */
58
+ if (red > 128 && blue > 128)
59
+ return 'carries';
60
+ /* Neither did, which no encode of this colour can produce. */
61
+ if (red < 32 && blue < 32)
62
+ return 'empty';
63
+ return 'unknown';
64
+ }
65
+ /** One answer per session. See `framesReachEncoder`. */
66
+ let asked = null;
67
+ /**
68
+ * Paint a known colour, encode it, decode it back, and report what came out.
69
+ *
70
+ * Memoised for the session: the fault is a property of how the browser was started, so a second
71
+ * answer cannot differ from the first, and an export path that asks before every attempt should
72
+ * pay for one encode rather than one per press.
73
+ *
74
+ * Every failure path answers `unknown` rather than throwing, including the timeout. This runs in
75
+ * front of an export, and a capability probe that can itself stop one has become a liability
76
+ * rather than a check.
77
+ */
78
+ export function framesReachEncoder() {
79
+ asked ??= run();
80
+ return asked;
81
+ }
82
+ async function run() {
83
+ if (typeof document === 'undefined' ||
84
+ typeof VideoEncoder !== 'function' ||
85
+ typeof VideoDecoder !== 'function' ||
86
+ typeof VideoFrame !== 'function') {
87
+ return 'unknown';
88
+ }
89
+ try {
90
+ return await Promise.race([
91
+ roundTrip(),
92
+ new Promise((resolve) => {
93
+ setTimeout(() => resolve('unknown'), PROBE_TIMEOUT_MS);
94
+ }),
95
+ ]);
96
+ }
97
+ catch {
98
+ return 'unknown';
99
+ }
100
+ }
101
+ async function roundTrip() {
102
+ const codec = clipCodecs(PROBE_SIZE, PROBE_SIZE)[0];
103
+ if (codec === undefined)
104
+ return 'unknown';
105
+ const canvas = document.createElement('canvas');
106
+ canvas.width = PROBE_SIZE;
107
+ canvas.height = PROBE_SIZE;
108
+ const ctx = canvas.getContext('2d', { alpha: false });
109
+ if (ctx === null)
110
+ return 'unknown';
111
+ ctx.fillStyle = PROBE_COLOR;
112
+ ctx.fillRect(0, 0, PROBE_SIZE, PROBE_SIZE);
113
+ const chunks = [];
114
+ /* The type the decoder's own config field takes, which is wider than `BufferSource`: it
115
+ accommodates a `SharedArrayBuffer`, and narrowing it here would only be a cast back. */
116
+ let description;
117
+ let failed = false;
118
+ const config = {
119
+ codec,
120
+ width: PROBE_SIZE,
121
+ height: PROBE_SIZE,
122
+ bitrate: 1_000_000,
123
+ framerate: 30,
124
+ };
125
+ const support = await VideoEncoder.isConfigSupported(config);
126
+ if (support.supported !== true)
127
+ return 'unknown';
128
+ const encoder = new VideoEncoder({
129
+ output: (chunk, meta) => {
130
+ if (description === undefined && meta?.decoderConfig?.description !== undefined) {
131
+ description = meta.decoderConfig.description;
132
+ }
133
+ chunks.push(chunk);
134
+ },
135
+ error: () => {
136
+ failed = true;
137
+ },
138
+ });
139
+ encoder.configure(config);
140
+ const frame = new VideoFrame(canvas, { timestamp: 0, duration: 33_333 });
141
+ try {
142
+ encoder.encode(frame, { keyFrame: true });
143
+ }
144
+ finally {
145
+ frame.close();
146
+ }
147
+ await encoder.flush();
148
+ encoder.close();
149
+ if (failed || chunks.length === 0)
150
+ return 'unknown';
151
+ let decoded = null;
152
+ const decoder = new VideoDecoder({
153
+ output: (picture) => {
154
+ try {
155
+ const read = document.createElement('canvas');
156
+ read.width = PROBE_SIZE;
157
+ read.height = PROBE_SIZE;
158
+ const readCtx = read.getContext('2d', { alpha: false });
159
+ if (readCtx !== null) {
160
+ readCtx.drawImage(picture, 0, 0);
161
+ const px = readCtx.getImageData(PROBE_SIZE / 2, PROBE_SIZE / 2, 1, 1).data;
162
+ decoded = [px[0] ?? 0, px[1] ?? 0, px[2] ?? 0];
163
+ }
164
+ }
165
+ finally {
166
+ picture.close();
167
+ }
168
+ },
169
+ error: () => {
170
+ failed = true;
171
+ },
172
+ });
173
+ decoder.configure({
174
+ codec,
175
+ codedWidth: PROBE_SIZE,
176
+ codedHeight: PROBE_SIZE,
177
+ ...(description === undefined ? {} : { description }),
178
+ });
179
+ for (const chunk of chunks)
180
+ decoder.decode(chunk);
181
+ await decoder.flush();
182
+ decoder.close();
183
+ if (failed)
184
+ return 'unknown';
185
+ return deliveryVerdict(decoded);
186
+ }
@@ -0,0 +1,13 @@
1
+ /*! DriftEngine | Copyright 2026 Drift Technologies | Apache-2.0 | https://github.com/drftrun/driftengine */
2
+ /**
3
+ * Recording what the engine draws: encoding a clip, and delivering frames to it.
4
+ *
5
+ * The muxer dependency lives here rather than in core, which is the point — a game that
6
+ * records nothing installs no muxer.
7
+ */
8
+ export { ClipEncoder, frameTimestampUs } from './clipEncoder.ts';
9
+ export type { ClipAudioCodec, ClipEncoderOptions } from './clipEncoder.ts';
10
+ export { clipEncodingSupported, offlineEncodingSupported } from './clipSupport.ts';
11
+ export type { ClipEncodeRequest } from './clipSupport.ts';
12
+ export { framesReachEncoder } from './frameDelivery.ts';
13
+ export type { FrameDelivery } from './frameDelivery.ts';
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /*! DriftEngine | Copyright 2026 Drift Technologies | Apache-2.0 | https://github.com/drftrun/driftengine */
2
+ /**
3
+ * Recording what the engine draws: encoding a clip, and delivering frames to it.
4
+ *
5
+ * The muxer dependency lives here rather than in core, which is the point — a game that
6
+ * records nothing installs no muxer.
7
+ */
8
+ export { ClipEncoder, frameTimestampUs } from './clipEncoder.js';
9
+ export { clipEncodingSupported, offlineEncodingSupported } from './clipSupport.js';
10
+ export { framesReachEncoder } from './frameDelivery.js';
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@driftengine/media",
3
+ "version": "3.61.0",
4
+ "description": "Clip encoding and frame delivery for recording what the engine draws",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "drift-source": "./src/index.ts",
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json",
16
+ "./*": "./*"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "!src/**/*.test.ts",
22
+ "!src/**/*.test.mjs",
23
+ "!src/**/__snapshots__",
24
+ "README.md",
25
+ "LICENSE",
26
+ "NOTICE"
27
+ ],
28
+ "sideEffects": false,
29
+ "dependencies": {
30
+ "mp4-muxer": "5.2.2"
31
+ },
32
+ "peerDependencies": {
33
+ "@driftengine/core": "3.61.0"
34
+ },
35
+ "author": "Drift Technologies",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/drftrun/driftengine.git",
39
+ "directory": "packages/media"
40
+ },
41
+ "homepage": "https://github.com/drftrun/driftengine#readme",
42
+ "bugs": "https://github.com/drftrun/driftengine/issues",
43
+ "keywords": [
44
+ "driftengine",
45
+ "3d",
46
+ "webgl",
47
+ "webgpu",
48
+ "typescript",
49
+ "video",
50
+ "recording",
51
+ "mp4",
52
+ "encoding"
53
+ ],
54
+ "engines": {
55
+ "node": ">=22.12.0"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ }
60
+ }