@remedia/node 0.1.0-dev.4 → 0.1.0-dev.5

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,499 @@
1
+ import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, realpathSync, } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
4
+ /** Command that installs FFmpeg 9 on Linux and macOS with the Remedia installer. */
5
+ export const UNIX_INSTALL_COMMAND = 'curl -fsSL https://raw.githubusercontent.com/twlite/remedia/main/scripts/install.sh | sh -s -- --ffmpeg-only';
6
+ /** Command that installs FFmpeg 9 on Windows PowerShell with the Remedia installer. */
7
+ export const WINDOWS_INSTALL_COMMAND = '& ([scriptblock]::Create((irm https://raw.githubusercontent.com/twlite/remedia/main/scripts/install.ps1))) -FFmpegOnly';
8
+ /**
9
+ * Raised when no compatible FFmpeg 9 installation could be loaded, or when an explicit
10
+ * `REMEDIA_FFMPEG_DIR` override is unusable. The message lists every path considered and how to
11
+ * install or point to FFmpeg.
12
+ */
13
+ export class FfmpegLoadError extends Error {
14
+ /** Every candidate that was examined, in order. */
15
+ candidates;
16
+ /**
17
+ * @param candidates - Reports for each installation candidate that was examined.
18
+ * @param explicitOverride - Whether `REMEDIA_FFMPEG_DIR` was set, which disables fallbacks.
19
+ * @param platform - Platform used to choose the install command shown in the message.
20
+ */
21
+ constructor(candidates, explicitOverride, platform = process.platform) {
22
+ super(describeFailure(candidates, explicitOverride, platform));
23
+ this.name = 'FfmpegLoadError';
24
+ this.candidates = candidates;
25
+ }
26
+ }
27
+ /** FFmpeg 9 shared libraries in dependency order (leaf libraries first) with their ABI majors. */
28
+ export const FFMPEG_LIBRARIES = [
29
+ { name: 'avutil', major: 61 },
30
+ { name: 'swresample', major: 7 },
31
+ { name: 'avcodec', major: 63 },
32
+ { name: 'avformat', major: 63 },
33
+ { name: 'avfilter', major: 12 },
34
+ ];
35
+ /**
36
+ * The current process as a {@link DiscoveryEnvironment}.
37
+ *
38
+ * @returns Facts read from `process` and the operating system.
39
+ * @internal
40
+ */
41
+ export function currentEnvironment() {
42
+ return {
43
+ platform: process.platform,
44
+ arch: process.arch,
45
+ env: process.env,
46
+ homeDirectory: homedir(),
47
+ execPath: process.execPath,
48
+ };
49
+ }
50
+ /**
51
+ * File name of one FFmpeg library at its ABI major on a platform.
52
+ *
53
+ * @param name - FFmpeg library name without the `lib` prefix.
54
+ * @param major - ABI major version.
55
+ * @param platform - Node.js platform string.
56
+ * @returns For example `libavcodec.so.63`, `libavcodec.63.dylib` or `avcodec-63.dll`.
57
+ * @internal
58
+ */
59
+ export function libraryFileName(name, major, platform) {
60
+ if (platform === 'win32')
61
+ return `${name}-${major}.dll`;
62
+ if (platform === 'darwin')
63
+ return `lib${name}.${major}.dylib`;
64
+ return `lib${name}.so.${major}`;
65
+ }
66
+ /**
67
+ * Directories of a prefix that may hold the shared libraries, most specific first.
68
+ *
69
+ * @param root - A prefix such as `~/.remedia/ffmpeg9`, or the library directory itself.
70
+ * @param platform - Node.js platform string.
71
+ * @returns Candidate library directories inside `root`.
72
+ * @internal
73
+ */
74
+ export function layoutDirectories(root, platform) {
75
+ if (platform === 'win32')
76
+ return [join(root, 'Library', 'bin'), join(root, 'bin'), root];
77
+ return [join(root, 'lib'), join(root, 'lib64'), root];
78
+ }
79
+ /**
80
+ * Default location of the installer-managed FFmpeg prefix for the executing user.
81
+ *
82
+ * @param environment - Process facts; only the home directory is used.
83
+ * @returns `<home>/.remedia/ffmpeg9`.
84
+ * @internal
85
+ */
86
+ export function managedPrefix(environment) {
87
+ return join(environment.homeDirectory, '.remedia', 'ffmpeg9');
88
+ }
89
+ function splitPathList(value) {
90
+ return (value ?? '')
91
+ .split(delimiter)
92
+ .map((entry) => entry.trim().replace(/^"(.*)"$/, '$1'))
93
+ .filter((entry) => entry.length > 0 && isAbsolute(entry));
94
+ }
95
+ function readLoaderConfiguration(file, depth = 0) {
96
+ if (depth > 4)
97
+ return [];
98
+ let contents;
99
+ try {
100
+ contents = readFileSync(file, 'utf8');
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ const directories = [];
106
+ for (const rawLine of contents.split('\n')) {
107
+ const line = rawLine.replace(/#.*$/, '').trim();
108
+ if (line.length === 0)
109
+ continue;
110
+ if (line.startsWith('include ')) {
111
+ const pattern = line.slice('include '.length).trim();
112
+ const directory = dirname(pattern);
113
+ const matcher = new RegExp(`^${pattern
114
+ .slice(directory.length + 1)
115
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
116
+ .replace(/\*/g, '.*')
117
+ .replace(/\?/g, '.')}$`);
118
+ try {
119
+ for (const entry of readdirSync(directory).sort()) {
120
+ if (matcher.test(entry)) {
121
+ directories.push(...readLoaderConfiguration(join(directory, entry), depth + 1));
122
+ }
123
+ }
124
+ }
125
+ catch {
126
+ // A missing include directory is normal.
127
+ }
128
+ }
129
+ else if (isAbsolute(line)) {
130
+ directories.push(line);
131
+ }
132
+ }
133
+ return directories;
134
+ }
135
+ /**
136
+ * Directories the platform's own library search would consult, for finding an existing
137
+ * system-wide FFmpeg without invoking the loader (which could half-load a broken installation).
138
+ *
139
+ * The current directory is never included, so a planted library there cannot be selected.
140
+ *
141
+ * @param environment - Process facts.
142
+ * @returns De-duplicated absolute directories in search order.
143
+ * @internal
144
+ */
145
+ export function systemDirectories(environment) {
146
+ const { platform, arch, env } = environment;
147
+ let directories = [];
148
+ if (platform === 'win32') {
149
+ directories = [dirname(environment.execPath), ...splitPathList(env.PATH ?? env.Path)];
150
+ }
151
+ else if (platform === 'darwin') {
152
+ directories = [
153
+ ...splitPathList(env.DYLD_LIBRARY_PATH),
154
+ ...splitPathList(env.DYLD_FALLBACK_LIBRARY_PATH),
155
+ '/opt/homebrew/opt/ffmpeg/lib',
156
+ '/usr/local/opt/ffmpeg/lib',
157
+ '/opt/homebrew/lib',
158
+ '/usr/local/lib',
159
+ ];
160
+ }
161
+ else if (platform === 'linux') {
162
+ const multiarch = arch === 'x64'
163
+ ? 'x86_64-linux-gnu'
164
+ : arch === 'arm64'
165
+ ? 'aarch64-linux-gnu'
166
+ : arch === 'arm'
167
+ ? 'arm-linux-gnueabihf'
168
+ : null;
169
+ directories = [
170
+ ...splitPathList(env.LD_LIBRARY_PATH),
171
+ ...readLoaderConfiguration('/etc/ld.so.conf'),
172
+ '/usr/local/lib',
173
+ ...(multiarch ? [`/usr/lib/${multiarch}`, `/lib/${multiarch}`] : []),
174
+ '/usr/lib64',
175
+ '/usr/lib',
176
+ '/lib64',
177
+ '/lib',
178
+ ];
179
+ }
180
+ else if (platform === 'freebsd') {
181
+ directories = [...splitPathList(env.LD_LIBRARY_PATH), '/usr/local/lib', '/usr/lib', '/lib'];
182
+ }
183
+ else {
184
+ directories = splitPathList(env.LD_LIBRARY_PATH);
185
+ }
186
+ return [...new Set(directories.map((directory) => resolve(directory)))];
187
+ }
188
+ /**
189
+ * Architectures a native binary is built for, read from its file header.
190
+ *
191
+ * @param file - Path to an ELF, Mach-O (thin or universal) or PE file.
192
+ * @returns Node.js `process.arch` names, or `null` when the format is not recognized.
193
+ * @internal
194
+ */
195
+ export function binaryArchitectures(file) {
196
+ const header = Buffer.alloc(65_536);
197
+ let length;
198
+ let descriptor = null;
199
+ try {
200
+ descriptor = openSync(file, 'r');
201
+ length = readSync(descriptor, header, 0, header.length, 0);
202
+ }
203
+ catch {
204
+ return null;
205
+ }
206
+ finally {
207
+ if (descriptor !== null)
208
+ closeSync(descriptor);
209
+ }
210
+ if (length < 20)
211
+ return null;
212
+ const mapCpu = (table, value) => table[value] ? [table[value]] : [];
213
+ // ELF
214
+ if (header.readUInt32BE(0) === 0x7f454c46) {
215
+ const little = header[5] === 1;
216
+ const machine = little ? header.readUInt16LE(18) : header.readUInt16BE(18);
217
+ return mapCpu({ 62: 'x64', 183: 'arm64', 3: 'ia32', 40: 'arm' }, machine);
218
+ }
219
+ // Mach-O thin (little endian host order for every supported Apple CPU)
220
+ const magic = header.readUInt32LE(0);
221
+ if (magic === 0xfeedfacf || magic === 0xfeedface) {
222
+ return mapCpu({ 0x01000007: 'x64', 0x0100000c: 'arm64', 7: 'ia32', 12: 'arm' }, header.readUInt32LE(4));
223
+ }
224
+ // Mach-O universal (always big endian)
225
+ if (header.readUInt32BE(0) === 0xcafebabe || header.readUInt32BE(0) === 0xcafebabf) {
226
+ const wide = header.readUInt32BE(0) === 0xcafebabf;
227
+ const count = header.readUInt32BE(4);
228
+ const size = wide ? 32 : 20;
229
+ if (count === 0 || count > 16)
230
+ return null;
231
+ const architectures = [];
232
+ for (let index = 0; index < count && 8 + (index + 1) * size <= length; index += 1) {
233
+ architectures.push(...mapCpu({ 0x01000007: 'x64', 0x0100000c: 'arm64', 7: 'ia32', 12: 'arm' }, header.readUInt32BE(8 + index * size)));
234
+ }
235
+ return architectures;
236
+ }
237
+ // PE
238
+ if (header[0] === 0x4d && header[1] === 0x5a && length >= 0x40) {
239
+ const offset = header.readUInt32LE(0x3c);
240
+ if (offset + 6 <= length && header.readUInt32LE(offset) === 0x00004550) {
241
+ return mapCpu({ 0x8664: 'x64', 0xaa64: 'arm64', 0x14c: 'ia32', 0x1c4: 'arm' }, header.readUInt16LE(offset + 4));
242
+ }
243
+ }
244
+ return null;
245
+ }
246
+ function inspectDirectory(directory, platform) {
247
+ const missing = [];
248
+ const otherMajors = [];
249
+ const files = [];
250
+ let entries = [];
251
+ try {
252
+ entries = readdirSync(directory);
253
+ }
254
+ catch {
255
+ // Reported as missing libraries below.
256
+ }
257
+ for (const { name, major } of FFMPEG_LIBRARIES) {
258
+ const fileName = libraryFileName(name, major, platform);
259
+ const path = join(directory, fileName);
260
+ if (existsSync(path)) {
261
+ files.push({ name, major, path });
262
+ continue;
263
+ }
264
+ missing.push(fileName);
265
+ const pattern = platform === 'win32'
266
+ ? new RegExp(`^${name}-(\\d+)\\.dll$`, 'i')
267
+ : platform === 'darwin'
268
+ ? new RegExp(`^lib${name}\\.(\\d+)\\.dylib$`)
269
+ : new RegExp(`^lib${name}\\.so\\.(\\d+)`);
270
+ for (const entry of entries) {
271
+ const match = pattern.exec(entry);
272
+ if (match)
273
+ otherMajors.push(`lib${name} ABI ${match[1]} (requires ${major})`);
274
+ }
275
+ }
276
+ return {
277
+ files: missing.length === 0 ? files : null,
278
+ missing,
279
+ otherMajors: [...new Set(otherMajors)],
280
+ found: FFMPEG_LIBRARIES.length - missing.length,
281
+ };
282
+ }
283
+ function classifyLoaderFailure(result) {
284
+ const message = result.message ?? '';
285
+ // Only Windows reports numeric codes (ERROR_MOD_NOT_FOUND and ERROR_BAD_EXE_FORMAT); Unix
286
+ // loaders report text, matched below.
287
+ if (result.code === 126)
288
+ return 'unresolved-dependency';
289
+ if (result.code === 193)
290
+ return 'architecture-mismatch';
291
+ if (/wrong ELF class|invalid ELF header|incompatible architecture|not a mach-o|bad image/i.test(message)) {
292
+ return 'architecture-mismatch';
293
+ }
294
+ if (/cannot open shared object file|library not loaded|image not found|module could not be found/i.test(message)) {
295
+ return 'unresolved-dependency';
296
+ }
297
+ return 'load-failed';
298
+ }
299
+ function stripExtendedPathPrefix(path) {
300
+ return path.startsWith('\\\\?\\') ? path.slice(4) : path;
301
+ }
302
+ function formatVersion(packed) {
303
+ return `${packed >>> 16}.${(packed >>> 8) & 0xff}.${packed & 0xff}`;
304
+ }
305
+ /**
306
+ * Load one directory's FFmpeg libraries through the bootstrap, all or nothing.
307
+ *
308
+ * Nothing stays loaded after a failure, so a later candidate can never bind to libraries left over
309
+ * from a rejected one.
310
+ *
311
+ * @param bootstrap - The FFmpeg-free native bootstrap.
312
+ * @param directory - Directory that should contain all five libraries.
313
+ * @param environment - Process facts.
314
+ * @returns The loaded libraries, or the classified rejection.
315
+ * @internal
316
+ */
317
+ export function loadDirectory(bootstrap, directory, environment) {
318
+ const inspection = inspectDirectory(directory, environment.platform);
319
+ if (!inspection.files) {
320
+ const others = inspection.otherMajors.length > 0 ? ` Found: ${inspection.otherMajors.join(', ')}.` : '';
321
+ return {
322
+ ok: false,
323
+ reason: inspection.otherMajors.length > 0 ? 'abi-mismatch' : 'missing-libraries',
324
+ detail: `Missing ${inspection.missing.join(', ')}.${others}`,
325
+ };
326
+ }
327
+ for (const { path } of inspection.files) {
328
+ const architectures = binaryArchitectures(path);
329
+ if (architectures !== null && !architectures.includes(environment.arch)) {
330
+ return {
331
+ ok: false,
332
+ reason: 'architecture-mismatch',
333
+ detail: `${path} is built for ${architectures.join('/') || 'an unknown CPU'}, but this process is ${environment.arch}.`,
334
+ };
335
+ }
336
+ }
337
+ const libraries = [];
338
+ const fail = (rejection) => {
339
+ bootstrap.rollbackLoaded();
340
+ return { ok: false, ...rejection };
341
+ };
342
+ for (const { name, major, path } of inspection.files) {
343
+ const result = bootstrap.loadLibrary(path, `${name}_version`);
344
+ if (!result.ok) {
345
+ return fail({
346
+ reason: classifyLoaderFailure(result),
347
+ detail: `${path}: ${result.message ?? 'the loader rejected the library'}`,
348
+ });
349
+ }
350
+ const packed = result.version ?? 0;
351
+ if (packed >>> 16 !== major) {
352
+ return fail({
353
+ reason: 'abi-mismatch',
354
+ detail: `${path} reports lib${name} ABI ${packed >>> 16}, but ${major} is required.`,
355
+ });
356
+ }
357
+ let loadedPath = stripExtendedPathPrefix(result.path ?? path);
358
+ try {
359
+ loadedPath = realpathSync(loadedPath);
360
+ }
361
+ catch {
362
+ // Keep the reported path.
363
+ }
364
+ libraries.push({ name, path: loadedPath, version: formatVersion(packed), major });
365
+ }
366
+ const directories = new Set(libraries.map((library) => dirname(library.path)));
367
+ if (directories.size > 1) {
368
+ return fail({
369
+ reason: 'mixed-installations',
370
+ detail: `The FFmpeg libraries resolved to different directories: ${[...directories].join(', ')}.`,
371
+ });
372
+ }
373
+ bootstrap.commitLoaded();
374
+ return { ok: true, libraries, directory: dirname(libraries[0].path) };
375
+ }
376
+ /**
377
+ * Choose and load an FFmpeg 9 installation.
378
+ *
379
+ * Precedence: `REMEDIA_FFMPEG_DIR`, then an installation visible to the system, then the managed
380
+ * prefix `~/.remedia/ffmpeg9`. An explicit override that cannot be loaded is an error; the other
381
+ * steps are never consulted in that case.
382
+ *
383
+ * @param bootstrap - The FFmpeg-free native bootstrap.
384
+ * @param environment - Process facts; defaults to the current process.
385
+ * @returns The selected runtime.
386
+ * @throws {FfmpegLoadError} If no compatible installation can be loaded.
387
+ * @internal
388
+ */
389
+ export function selectFfmpeg(bootstrap, environment = currentEnvironment()) {
390
+ const considered = [];
391
+ const attempt = (source, directory) => {
392
+ const result = loadDirectory(bootstrap, directory, environment);
393
+ if (result.ok) {
394
+ considered.push({ source, directory, outcome: 'selected' });
395
+ return {
396
+ source,
397
+ directory: result.directory,
398
+ libraries: result.libraries,
399
+ considered,
400
+ };
401
+ }
402
+ considered.push({
403
+ source,
404
+ directory,
405
+ outcome: 'rejected',
406
+ reason: result.reason,
407
+ detail: result.detail,
408
+ });
409
+ return null;
410
+ };
411
+ const override = environment.env.REMEDIA_FFMPEG_DIR?.trim();
412
+ if (override) {
413
+ const root = resolve(override);
414
+ if (!existsSync(root)) {
415
+ considered.push({
416
+ source: 'explicit',
417
+ directory: root,
418
+ outcome: 'rejected',
419
+ reason: 'not-found',
420
+ detail: 'REMEDIA_FFMPEG_DIR does not exist.',
421
+ });
422
+ throw new FfmpegLoadError(considered, true, environment.platform);
423
+ }
424
+ // The override may name the installation prefix or the library directory itself. A directory
425
+ // that is already complete wins; otherwise the first layout is used so its report is shown.
426
+ const layouts = layoutDirectories(root, environment.platform);
427
+ const directory = layouts.find((candidate) => inspectDirectory(candidate, environment.platform).files) ??
428
+ layouts.find((candidate) => existsSync(candidate)) ??
429
+ root;
430
+ const selected = attempt('explicit', directory);
431
+ if (selected)
432
+ return selected;
433
+ throw new FfmpegLoadError(considered, true, environment.platform);
434
+ }
435
+ const searched = environment.systemDirectories
436
+ ? [...environment.systemDirectories]
437
+ : systemDirectories(environment);
438
+ // A directory is worth reporting if it holds any FFmpeg library, including other ABI majors, so
439
+ // that an FFmpeg 7 installation is explained rather than looking like "nothing installed".
440
+ const withLibraries = searched.filter((directory) => {
441
+ const inspection = inspectDirectory(directory, environment.platform);
442
+ return inspection.found > 0 || inspection.otherMajors.length > 0;
443
+ });
444
+ for (const directory of withLibraries) {
445
+ const selected = attempt('system', directory);
446
+ if (selected)
447
+ return selected;
448
+ }
449
+ if (withLibraries.length === 0) {
450
+ considered.push({
451
+ source: 'system',
452
+ directory: null,
453
+ outcome: 'rejected',
454
+ reason: 'not-found',
455
+ detail: `No FFmpeg 9 libraries in ${searched.length} system library directories (${searched.join(', ')}).`,
456
+ });
457
+ }
458
+ const managed = managedPrefix(environment);
459
+ if (!existsSync(managed)) {
460
+ considered.push({
461
+ source: 'managed',
462
+ directory: managed,
463
+ outcome: 'rejected',
464
+ reason: 'not-found',
465
+ detail: 'The Remedia installer has not installed FFmpeg here.',
466
+ });
467
+ }
468
+ else {
469
+ const layouts = layoutDirectories(managed, environment.platform);
470
+ const directory = layouts.find((candidate) => inspectDirectory(candidate, environment.platform).files) ??
471
+ layouts[0];
472
+ const selected = attempt('managed', directory);
473
+ if (selected)
474
+ return selected;
475
+ }
476
+ throw new FfmpegLoadError(considered, false, environment.platform);
477
+ }
478
+ function describeFailure(candidates, explicitOverride, platform) {
479
+ const required = FFMPEG_LIBRARIES.map(({ name, major }) => `lib${name} ${major}`).join(', ');
480
+ const lines = [
481
+ explicitOverride
482
+ ? 'REMEDIA_FFMPEG_DIR does not point to a loadable FFmpeg 9 installation, and no other installation was tried.'
483
+ : 'Remedia could not load a compatible FFmpeg 9 runtime.',
484
+ `Required ABI majors: ${required}.`,
485
+ 'Considered:',
486
+ ];
487
+ for (const candidate of candidates) {
488
+ const label = candidate.source === 'explicit' ? 'REMEDIA_FFMPEG_DIR' : candidate.source;
489
+ const location = candidate.directory ?? '(see detail)';
490
+ const status = candidate.outcome === 'selected'
491
+ ? 'selected'
492
+ : `rejected${candidate.reason ? ` (${candidate.reason})` : ''}`;
493
+ lines.push(` - ${label}: ${location} -> ${status}${candidate.detail ? `: ${candidate.detail}` : ''}`);
494
+ }
495
+ lines.push('', 'Install FFmpeg 9 with the Remedia installer:', ` ${platform === 'win32' ? WINDOWS_INSTALL_COMMAND : UNIX_INSTALL_COMMAND}`, platform === 'win32'
496
+ ? 'To use another FFmpeg 9 prefix, set REMEDIA_FFMPEG_DIR to the directory that contains Library\\bin.'
497
+ : 'To use another FFmpeg 9 prefix, set REMEDIA_FFMPEG_DIR to the directory that contains lib/.');
498
+ return lines.join('\n');
499
+ }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,10 @@ export type { DiscordGatewayPacket, DiscordGatewaySend, DiscordGatewayVoicePaylo
4
4
  export { PlaybackSession } from './playback.js';
5
5
  export { PlaybackHooks, PlaybackSessionEvents, RemediaEvents, VoiceSessionEvents, } from './lifecycle.js';
6
6
  export { Remedia, type ProbeOptions, type RemediaOptions } from './remedia.js';
7
+ export { getFfmpegRuntime } from './native-binding.js';
8
+ export { FfmpegLoadError } from './ffmpeg-runtime.js';
9
+ export type { FfmpegCandidateReport, FfmpegFailureReason, FfmpegLibraryInfo, FfmpegRuntimeInfo, FfmpegSource, } from './ffmpeg-runtime.js';
7
10
  export type { MediaSource } from './input.js';
8
11
  export type { AudioStreamInfo, DebugListener, DiagnosticEvent, DiagnosticFieldValue, DiagnosticSeverity, PlaybackPosition, PlaybackState, VoiceConnectionState, } from './types.js';
9
12
  export type { CrossfadeOptions, CrossfadeContinuation, CrossfadeTransition, CrossfadeStageEvent, StageNextOptions, StageSnapshot, BeforePlayContext, BeforePlayHook, BeforePlayResult, LifecycleVoiceState, PlayOptions, PlaybackEventIdentity, PlaybackLifecycleEvents, PlaybackSessionEventMap, RemediaLifecycleEvents, SessionLifecycleEvents, VoiceEventIdentity, VoiceLifecycleEvents, } from './lifecycle.js';
13
+ export type { ResourceMemory, ResourceCpu, ProcessResourceStatistics } from './resources.js';
package/dist/index.js CHANGED
@@ -2,3 +2,5 @@ export { DiscordSession } from './discord.js';
2
2
  export { PlaybackSession } from './playback.js';
3
3
  export { PlaybackHooks, PlaybackSessionEvents, RemediaEvents, VoiceSessionEvents, } from './lifecycle.js';
4
4
  export { Remedia } from './remedia.js';
5
+ export { getFfmpegRuntime } from './native-binding.js';
6
+ export { FfmpegLoadError } from './ffmpeg-runtime.js';
@@ -0,0 +1,26 @@
1
+ import { type FfmpegRuntimeInfo } from './ffmpeg-runtime.js';
2
+ import type { NativeRemedia as NativeRemediaClass } from '../native.js';
3
+ /** The Remedia native engine class, loaded after FFmpeg 9 was located. */
4
+ export declare const NativeRemedia: typeof NativeRemediaClass;
5
+ /** Instance type of the Remedia native engine. */
6
+ export type NativeRemedia = NativeRemediaClass;
7
+ /**
8
+ * Report the FFmpeg 9 installation `@remedia/node` loaded.
9
+ *
10
+ * Importing the package already selects and loads FFmpeg, so this never triggers a search or a
11
+ * download and is cheap to call. It is independent of Discord, playback and the standalone server.
12
+ *
13
+ * @returns The selected source and directory, the loaded library files and versions, and every
14
+ * candidate discovery examined. When the platform has no library bootstrap the source is
15
+ * `unmanaged` and the library list is empty.
16
+ * @throws {Error} Only if called before the native addon finished loading, which cannot happen
17
+ * after the package has been imported successfully.
18
+ * @example
19
+ * ```ts
20
+ * import { getFfmpegRuntime } from '@remedia/node';
21
+ *
22
+ * const { source, directory, libraries } = getFfmpegRuntime();
23
+ * console.log(source, directory, libraries.map((library) => `${library.name} ${library.version}`));
24
+ * ```
25
+ */
26
+ export declare function getFfmpegRuntime(): FfmpegRuntimeInfo;
@@ -0,0 +1,80 @@
1
+ import { createRequire } from 'node:module';
2
+ import { dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { UNIX_INSTALL_COMMAND, WINDOWS_INSTALL_COMMAND, selectFfmpeg, } from './ffmpeg-runtime.js';
5
+ import { bootstrapCandidates, platformSuffixes } from './bootstrap-locator.js';
6
+ const require = createRequire(import.meta.url);
7
+ /** Interface version of the bootstrap addon this loader was written against. */
8
+ const BOOTSTRAP_VERSION = 1;
9
+ function locateBootstrap() {
10
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
11
+ const suffixes = platformSuffixes(process.platform, process.arch);
12
+ for (const candidate of bootstrapCandidates(packageRoot, suffixes)) {
13
+ try {
14
+ const binding = require(candidate);
15
+ if (binding.bootstrapVersion() === BOOTSTRAP_VERSION)
16
+ return binding;
17
+ }
18
+ catch {
19
+ // An unusable bootstrap is treated like a missing one; the OS loader is the fallback.
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+ function looksLikeMissingLibrary(error) {
25
+ const message = error instanceof Error ? error.message : String(error);
26
+ return /libav|libsw|avcodec|avformat|avutil|avfilter|swresample|ffmpeg|\.dylib|\.so\b|specified module|dll/i.test(message);
27
+ }
28
+ let runtime = null;
29
+ async function loadNativeBinding() {
30
+ const bootstrap = locateBootstrap();
31
+ const selected = bootstrap
32
+ ? selectFfmpeg(bootstrap)
33
+ : { source: 'unmanaged', directory: null, libraries: [], considered: [] };
34
+ let binding;
35
+ try {
36
+ binding = await import('../native.js');
37
+ }
38
+ catch (error) {
39
+ const hint = selected.source === 'unmanaged' && looksLikeMissingLibrary(error)
40
+ ? `\n\nThis platform has no Remedia library bootstrap, so the system library loader must find FFmpeg 9 itself. ` +
41
+ `Install it with the Remedia installer (${process.platform === 'win32' ? WINDOWS_INSTALL_COMMAND : UNIX_INSTALL_COMMAND}) and add its library directory to your loader search path.`
42
+ : '';
43
+ const origin = selected.source === 'unmanaged'
44
+ ? 'The Remedia native addon could not be loaded'
45
+ : `FFmpeg 9 was loaded from ${selected.directory} (${selected.source}), but the Remedia native addon could not be loaded`;
46
+ throw new Error(`${origin}: ${error instanceof Error ? error.message : String(error)}${hint}`, {
47
+ cause: error,
48
+ });
49
+ }
50
+ // Only now that the engine addon has loaded against the libraries is the runtime reported.
51
+ runtime = Object.freeze({ ...selected });
52
+ return binding;
53
+ }
54
+ const binding = await loadNativeBinding();
55
+ /** The Remedia native engine class, loaded after FFmpeg 9 was located. */
56
+ export const NativeRemedia = binding.NativeRemedia;
57
+ /**
58
+ * Report the FFmpeg 9 installation `@remedia/node` loaded.
59
+ *
60
+ * Importing the package already selects and loads FFmpeg, so this never triggers a search or a
61
+ * download and is cheap to call. It is independent of Discord, playback and the standalone server.
62
+ *
63
+ * @returns The selected source and directory, the loaded library files and versions, and every
64
+ * candidate discovery examined. When the platform has no library bootstrap the source is
65
+ * `unmanaged` and the library list is empty.
66
+ * @throws {Error} Only if called before the native addon finished loading, which cannot happen
67
+ * after the package has been imported successfully.
68
+ * @example
69
+ * ```ts
70
+ * import { getFfmpegRuntime } from '@remedia/node';
71
+ *
72
+ * const { source, directory, libraries } = getFfmpegRuntime();
73
+ * console.log(source, directory, libraries.map((library) => `${library.name} ${library.version}`));
74
+ * ```
75
+ */
76
+ export function getFfmpegRuntime() {
77
+ if (!runtime)
78
+ throw new Error('The Remedia native addon has not finished loading.');
79
+ return runtime;
80
+ }
package/dist/remedia.d.ts CHANGED
@@ -31,6 +31,12 @@ export declare class Remedia extends RemediaEventEmitter<RemediaLifecycleEvents>
31
31
  * @returns An initialized independent native engine context.
32
32
  */
33
33
  static create(options?: RemediaOptions): Promise<Remedia>;
34
+ /**
35
+ * Read cached native resources for the shared Node.js process. Refresh runs off
36
+ * the calling thread at most once per second; the first snapshot has null counters.
37
+ * @returns Process-wide byte and CPU-time counters, shared by all embedded instances.
38
+ */
39
+ processResources(): import('./resources.js').ProcessResourceStatistics;
34
40
  /**
35
41
  * Inspect a disposable source without creating playback or voice resources. Readables may be partially or fully consumed.
36
42
  * @param source - Encoded input; acquire a fresh source for subsequent playback.
package/dist/remedia.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createReadableInput, toNativeSource } from './input.js';
2
2
  import { toStreamInfo } from './types.js';
3
3
  import { RemediaEventEmitter } from './events.js';
4
- import { NativeRemedia } from '../native.js';
4
+ import { NativeRemedia } from './native-binding.js';
5
5
  import { DiscordSession } from './discord.js';
6
6
  import { DiscordGatewayAdapter } from './gateway.js';
7
7
  import { PlaybackSessionEvents as PlaybackEvent, RemediaEvents as RemediaEvent, VoiceSessionEvents as VoiceEvent, } from './lifecycle.js';
@@ -35,6 +35,15 @@ export class Remedia extends RemediaEventEmitter {
35
35
  static async create(options) {
36
36
  return new Remedia(await NativeRemedia.create(), options);
37
37
  }
38
+ /**
39
+ * Read cached native resources for the shared Node.js process. Refresh runs off
40
+ * the calling thread at most once per second; the first snapshot has null counters.
41
+ * @returns Process-wide byte and CPU-time counters, shared by all embedded instances.
42
+ */
43
+ processResources() {
44
+ this.#assertOpen();
45
+ return JSON.parse(this.#native.processResourcesJson());
46
+ }
38
47
  /**
39
48
  * Inspect a disposable source without creating playback or voice resources. Readables may be partially or fully consumed.
40
49
  * @param source - Encoded input; acquire a fresh source for subsequent playback.