@remedia/node 0.1.0-dev.3 → 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.
- package/README.md +21 -7
- package/dist/bootstrap-locator.d.ts +1 -0
- package/dist/bootstrap-locator.js +104 -0
- package/dist/discord.d.ts +14 -1
- package/dist/discord.js +57 -0
- package/dist/ffmpeg-runtime.d.ts +94 -0
- package/dist/ffmpeg-runtime.js +499 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/lifecycle.d.ts +103 -0
- package/dist/lifecycle.js +5 -0
- package/dist/native-binding.d.ts +26 -0
- package/dist/native-binding.js +80 -0
- package/dist/playback.d.ts +19 -1
- package/dist/playback.js +300 -13
- package/dist/remedia.d.ts +6 -0
- package/dist/remedia.js +15 -1
- package/dist/resources.d.ts +51 -0
- package/dist/resources.js +1 -0
- package/native.d.ts +48 -0
- package/native.js +56 -56
- package/package.json +19 -16
|
@@ -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
|
-
export type { BeforePlayContext, BeforePlayHook, BeforePlayResult, LifecycleVoiceState, PlayOptions, PlaybackEventIdentity, PlaybackLifecycleEvents, PlaybackSessionEventMap, RemediaLifecycleEvents, SessionLifecycleEvents, VoiceEventIdentity, VoiceLifecycleEvents, } from './lifecycle.js';
|
|
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';
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export declare const PlaybackSessionEvents: {
|
|
|
16
16
|
readonly playbackResumed: "playbackResumed";
|
|
17
17
|
/** Emitted after seeking to a source timeline position. */
|
|
18
18
|
readonly playbackSeeked: "playbackSeeked";
|
|
19
|
+
readonly prepareRequested: "prepareRequested";
|
|
20
|
+
readonly stageReady: "stageReady";
|
|
21
|
+
readonly stageFailed: "stageFailed";
|
|
22
|
+
readonly stageInvalidated: "stageInvalidated";
|
|
23
|
+
readonly transitionCancelled: "transitionCancelled";
|
|
19
24
|
/** Emitted when the configured session filtergraph changes. */
|
|
20
25
|
readonly filterChanged: "filterChanged";
|
|
21
26
|
/** Emitted when the configured session linear gain changes. */
|
|
@@ -56,6 +61,11 @@ export declare const RemediaEvents: {
|
|
|
56
61
|
readonly playbackResumed: "playbackResumed";
|
|
57
62
|
/** Emitted after seeking to a source timeline position. */
|
|
58
63
|
readonly playbackSeeked: "playbackSeeked";
|
|
64
|
+
readonly prepareRequested: "prepareRequested";
|
|
65
|
+
readonly stageReady: "stageReady";
|
|
66
|
+
readonly stageFailed: "stageFailed";
|
|
67
|
+
readonly stageInvalidated: "stageInvalidated";
|
|
68
|
+
readonly transitionCancelled: "transitionCancelled";
|
|
59
69
|
/** Emitted when the configured session filtergraph changes. */
|
|
60
70
|
readonly filterChanged: "filterChanged";
|
|
61
71
|
/** Emitted when the configured session linear gain changes. */
|
|
@@ -69,6 +79,80 @@ export type RemediaEvents = (typeof RemediaEvents)[keyof typeof RemediaEvents];
|
|
|
69
79
|
* Correlation fields identifying the session and source generation that produced a playback event.
|
|
70
80
|
* @typeParam Metadata - Application-owned metadata attached to playback generations.
|
|
71
81
|
*/
|
|
82
|
+
/** Session opt-in for continuous PCM crossfade playback. */
|
|
83
|
+
export interface CrossfadeOptions {
|
|
84
|
+
/** Explicit enablement; omitted sessions retain ordinary playback. */
|
|
85
|
+
enabled: true;
|
|
86
|
+
/** Overlap duration in milliseconds, 1000–30000; defaults to 5000. */
|
|
87
|
+
durationMs?: number;
|
|
88
|
+
/** Lead before overlap in milliseconds, 5000–120000; defaults to 30000. */
|
|
89
|
+
prepareBeforeMs?: number;
|
|
90
|
+
/** Equal-power curve; the only supported curve. */
|
|
91
|
+
curve?: 'equalPower';
|
|
92
|
+
}
|
|
93
|
+
/** Identify a particular queued occurrence. Revisions prevent stale stage
|
|
94
|
+
* responses from replacing newer queue decisions.
|
|
95
|
+
*/
|
|
96
|
+
export interface StageNextOptions<Metadata = unknown> {
|
|
97
|
+
/** Stable ID of this queued occurrence, including repeated tracks. */
|
|
98
|
+
entryId: string;
|
|
99
|
+
/** Monotonically increasing queue decision revision; non-negative safe integer. */
|
|
100
|
+
queueRevision: number;
|
|
101
|
+
/** Revision of the resolved source for this entry; defaults to zero. */
|
|
102
|
+
sourceRevision?: number;
|
|
103
|
+
/** Absolute Unix time in milliseconds after which a prepared source must not be promoted. */
|
|
104
|
+
expiresAtMs?: number;
|
|
105
|
+
/** Current source generation to fence delayed preparation results; defaults to the active generation. */
|
|
106
|
+
expectedGeneration?: number;
|
|
107
|
+
/** Application-owned metadata retained locally and attached to incoming lifecycle events. */
|
|
108
|
+
customMetadata?: Metadata;
|
|
109
|
+
}
|
|
110
|
+
/** Crossfade playback identity, state, or capability metadata. */
|
|
111
|
+
export interface StageSnapshot {
|
|
112
|
+
/** Current source generation to fence delayed preparation results; defaults to the active generation. */
|
|
113
|
+
expectedGeneration: number;
|
|
114
|
+
/** Stable ID of this queued occurrence, including repeated tracks. */
|
|
115
|
+
entryId: string;
|
|
116
|
+
/** Monotonically increasing queue decision revision; non-negative safe integer. */
|
|
117
|
+
queueRevision: number;
|
|
118
|
+
/** Revision of the resolved source for this entry; defaults to zero. */
|
|
119
|
+
sourceRevision: number;
|
|
120
|
+
/** Current preparation state; failed sources cannot be promoted. */
|
|
121
|
+
state: 'preparing' | 'ready' | 'failed';
|
|
122
|
+
/** Decoded PCM currently buffered, in milliseconds; bounded to two seconds. */
|
|
123
|
+
bufferedMs: number;
|
|
124
|
+
/** Selected stream metadata, or null until the decoder opens. */
|
|
125
|
+
info: AudioStreamInfo | null;
|
|
126
|
+
/** Preparation failure message, or null. */
|
|
127
|
+
error: string | null;
|
|
128
|
+
}
|
|
129
|
+
/** Crossfade playback identity, state, or capability metadata. */
|
|
130
|
+
export interface CrossfadeContinuation {
|
|
131
|
+
/** Discriminator identifying this lifecycle transition. */
|
|
132
|
+
type: 'promoted';
|
|
133
|
+
/** Source generation identifying the promoted playback. */
|
|
134
|
+
generation: number;
|
|
135
|
+
/** Stable ID of this queued occurrence, including repeated tracks. */
|
|
136
|
+
entryId: string;
|
|
137
|
+
}
|
|
138
|
+
/** Crossfade playback identity, state, or capability metadata. */
|
|
139
|
+
export interface CrossfadeTransition {
|
|
140
|
+
/** Discriminator identifying this lifecycle transition. */
|
|
141
|
+
type: 'crossfade';
|
|
142
|
+
/** Source generation that remains audible during the fade. */
|
|
143
|
+
outgoingGeneration: number;
|
|
144
|
+
/** Stable ID of this queued occurrence, including repeated tracks. */
|
|
145
|
+
entryId: string;
|
|
146
|
+
}
|
|
147
|
+
/** Crossfade playback identity, state, or capability metadata. */
|
|
148
|
+
export interface CrossfadeStageEvent extends PlaybackEventIdentity {
|
|
149
|
+
/** Stable ID of this queued occurrence, including repeated tracks. */
|
|
150
|
+
entryId: string;
|
|
151
|
+
/** Monotonically increasing queue decision revision; non-negative safe integer. */
|
|
152
|
+
queueRevision: number;
|
|
153
|
+
/** Revision of the resolved source for this entry; defaults to zero. */
|
|
154
|
+
sourceRevision: number;
|
|
155
|
+
}
|
|
72
156
|
export interface PlaybackEventIdentity<Metadata = unknown> {
|
|
73
157
|
/** Opaque Remedia session identity used to correlate events; distinct from the Discord bot user ID. */
|
|
74
158
|
sessionId: string;
|
|
@@ -95,6 +179,7 @@ export type PlaybackLifecycleEvents<Metadata = unknown> = {
|
|
|
95
179
|
state: 'playing';
|
|
96
180
|
/** Metadata for the audio stream that started playback. */
|
|
97
181
|
info: AudioStreamInfo;
|
|
182
|
+
transition?: CrossfadeTransition;
|
|
98
183
|
}
|
|
99
184
|
];
|
|
100
185
|
/** Emitted when a source generation reaches natural end-of-stream. */
|
|
@@ -104,6 +189,7 @@ export type PlaybackLifecycleEvents<Metadata = unknown> = {
|
|
|
104
189
|
state: 'completed';
|
|
105
190
|
/** Latest source and output timeline measurements in milliseconds. */
|
|
106
191
|
position: PlaybackPosition;
|
|
192
|
+
continuation?: CrossfadeContinuation;
|
|
107
193
|
}
|
|
108
194
|
];
|
|
109
195
|
/** Emitted when a source generation is explicitly stopped or cancelled. */
|
|
@@ -125,6 +211,23 @@ export type PlaybackLifecycleEvents<Metadata = unknown> = {
|
|
|
125
211
|
}
|
|
126
212
|
];
|
|
127
213
|
/** Emitted when playback is paused without replacing its source generation. */
|
|
214
|
+
[PlaybackSessionEvents.prepareRequested]: [
|
|
215
|
+
PlaybackEventIdentity<Metadata> & {
|
|
216
|
+
remainingMs: number;
|
|
217
|
+
transitionInMs: number;
|
|
218
|
+
}
|
|
219
|
+
];
|
|
220
|
+
[PlaybackSessionEvents.stageReady]: [CrossfadeStageEvent];
|
|
221
|
+
[PlaybackSessionEvents.stageFailed]: [CrossfadeStageEvent & {
|
|
222
|
+
reason: string;
|
|
223
|
+
}];
|
|
224
|
+
[PlaybackSessionEvents.stageInvalidated]: [CrossfadeStageEvent];
|
|
225
|
+
[PlaybackSessionEvents.transitionCancelled]: [
|
|
226
|
+
PlaybackEventIdentity<Metadata> & {
|
|
227
|
+
failedGeneration: number;
|
|
228
|
+
entryId: string;
|
|
229
|
+
}
|
|
230
|
+
];
|
|
128
231
|
[PlaybackSessionEvents.playbackPaused]: [
|
|
129
232
|
PlaybackEventIdentity<Metadata> & {
|
|
130
233
|
/** Playback or transport lifecycle state before this transition. */
|
package/dist/lifecycle.js
CHANGED
|
@@ -14,6 +14,11 @@ export const PlaybackSessionEvents = {
|
|
|
14
14
|
playbackResumed: 'playbackResumed',
|
|
15
15
|
/** Emitted after seeking to a source timeline position. */
|
|
16
16
|
playbackSeeked: 'playbackSeeked',
|
|
17
|
+
prepareRequested: 'prepareRequested',
|
|
18
|
+
stageReady: 'stageReady',
|
|
19
|
+
stageFailed: 'stageFailed',
|
|
20
|
+
stageInvalidated: 'stageInvalidated',
|
|
21
|
+
transitionCancelled: 'transitionCancelled',
|
|
17
22
|
/** Emitted when the configured session filtergraph changes. */
|
|
18
23
|
filterChanged: 'filterChanged',
|
|
19
24
|
/** Emitted when the configured session linear gain changes. */
|