@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,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/playback.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RemediaEventEmitter } from './events.js';
|
|
2
2
|
import { type MediaSource } from './input.js';
|
|
3
|
-
import { PlaybackHooks, type PlayOptions, type PlaybackSessionEventMap } from './lifecycle.js';
|
|
3
|
+
import { PlaybackHooks, type PlayOptions, type PlaybackSessionEventMap, type StageNextOptions, type CrossfadeOptions, type StageSnapshot } from './lifecycle.js';
|
|
4
4
|
import { type AudioStreamInfo, type PlaybackPosition, type PlaybackState } from './types.js';
|
|
5
5
|
import { FilterControls } from './filters.js';
|
|
6
6
|
/**
|
|
@@ -15,6 +15,24 @@ export declare class PlaybackSession<Metadata = unknown> extends RemediaEventEmi
|
|
|
15
15
|
readonly filters: FilterControls;
|
|
16
16
|
/** Stable opaque identity for this wrapper, suitable for correlating lifecycle events. */
|
|
17
17
|
get sessionId(): string;
|
|
18
|
+
/** Enable continuous PCM playback before starting a source. */
|
|
19
|
+
/**
|
|
20
|
+
* Configure overlapping PCM playback while idle.
|
|
21
|
+
* @param options - Explicit opt-in settings, or false to retain ordinary playback.
|
|
22
|
+
* @returns Nothing; throws if settings are invalid or playback is active.
|
|
23
|
+
*/
|
|
24
|
+
configureCrossfade(options: CrossfadeOptions | false): void;
|
|
25
|
+
/** Current preparation snapshot, or null when no stage is pending. */
|
|
26
|
+
get stagedSource(): StageSnapshot | null;
|
|
27
|
+
/** Cancel the queued stage and release its input. @returns Resolves after cancellation. */
|
|
28
|
+
clearStage(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Prepare one upcoming source without restarting playback or its output.
|
|
31
|
+
* @param source - Fresh media input; Readables and buffers require embedded execution.
|
|
32
|
+
* @param options - Queue occurrence, revision fencing, expiry and locally retained metadata.
|
|
33
|
+
* @returns Preparation snapshot; rejects disabled sessions, stale identities or invalid inputs.
|
|
34
|
+
*/
|
|
35
|
+
stageNext(source: MediaSource, options: StageNextOptions<Metadata>): Promise<StageSnapshot>;
|
|
18
36
|
/** Current playback lifecycle state. */
|
|
19
37
|
get state(): PlaybackState;
|
|
20
38
|
/** Latest source and output timeline measurements in milliseconds. */
|
package/dist/playback.js
CHANGED
|
@@ -26,6 +26,14 @@ export class PlaybackSession extends RemediaEventEmitter {
|
|
|
26
26
|
#abortListener;
|
|
27
27
|
#latestGeneration = 0;
|
|
28
28
|
#guildId;
|
|
29
|
+
#crossfadeEnabled = false;
|
|
30
|
+
#fadeDurationMs = 5000;
|
|
31
|
+
#crossfadeTimer;
|
|
32
|
+
#stageInput;
|
|
33
|
+
#stageEntryId;
|
|
34
|
+
#stageWork = Promise.resolve();
|
|
35
|
+
#stages = new Map();
|
|
36
|
+
#outgoingInput;
|
|
29
37
|
/**
|
|
30
38
|
* Construct this wrapper with its owning services. Use the owning engine or client factory for application code.
|
|
31
39
|
*
|
|
@@ -53,6 +61,123 @@ export class PlaybackSession extends RemediaEventEmitter {
|
|
|
53
61
|
get currentGeneration() {
|
|
54
62
|
return this.#latestGeneration;
|
|
55
63
|
}
|
|
64
|
+
/** Enable continuous PCM playback before starting a source. */
|
|
65
|
+
/**
|
|
66
|
+
* Configure overlapping PCM playback while idle.
|
|
67
|
+
* @param options - Explicit opt-in settings, or false to retain ordinary playback.
|
|
68
|
+
* @returns Nothing; throws if settings are invalid or playback is active.
|
|
69
|
+
*/
|
|
70
|
+
configureCrossfade(options) {
|
|
71
|
+
this.#assertActive();
|
|
72
|
+
if (options) {
|
|
73
|
+
if (options.enabled !== true ||
|
|
74
|
+
(options.curve !== undefined && options.curve !== 'equalPower')) {
|
|
75
|
+
throw new TypeError('crossfade requires enabled: true and the equalPower curve');
|
|
76
|
+
}
|
|
77
|
+
const durationMs = options.durationMs ?? 5000;
|
|
78
|
+
this.#native.playbackConfigureCrossfade(this.#playbackId, true, durationMs, options.prepareBeforeMs ?? 30000);
|
|
79
|
+
this.#fadeDurationMs = durationMs;
|
|
80
|
+
this.#crossfadeEnabled = true;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
this.#native.playbackConfigureCrossfade(this.#playbackId, false, 0, 0);
|
|
84
|
+
this.#crossfadeEnabled = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Current preparation snapshot, or null when no stage is pending. */
|
|
88
|
+
get stagedSource() {
|
|
89
|
+
const stage = this.#native.playbackStagedSource(this.#playbackId);
|
|
90
|
+
return stage ? this.#normalizeStage(stage) : null;
|
|
91
|
+
}
|
|
92
|
+
/** Cancel the queued stage and release its input. @returns Resolves after cancellation. */
|
|
93
|
+
async clearStage() {
|
|
94
|
+
const work = this.#stageWork.then(() => this.#clearStage());
|
|
95
|
+
this.#stageWork = work.then(() => { }, () => { });
|
|
96
|
+
return work;
|
|
97
|
+
}
|
|
98
|
+
async #clearStage() {
|
|
99
|
+
await this.#native.playbackClearStage(this.#playbackId);
|
|
100
|
+
this.#drainCrossfadeEvents();
|
|
101
|
+
await this.#stageInput?.cancel();
|
|
102
|
+
this.#stageInput = undefined;
|
|
103
|
+
this.#stageEntryId = undefined;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Prepare one upcoming source without restarting playback or its output.
|
|
107
|
+
* @param source - Fresh media input; Readables and buffers require embedded execution.
|
|
108
|
+
* @param options - Queue occurrence, revision fencing, expiry and locally retained metadata.
|
|
109
|
+
* @returns Preparation snapshot; rejects disabled sessions, stale identities or invalid inputs.
|
|
110
|
+
*/
|
|
111
|
+
async stageNext(source, options) {
|
|
112
|
+
const work = this.#stageWork.then(() => this.#submitStage(source, options));
|
|
113
|
+
this.#stageWork = work.then(() => { }, () => { });
|
|
114
|
+
return work;
|
|
115
|
+
}
|
|
116
|
+
async #submitStage(source, options) {
|
|
117
|
+
this.#assertActive();
|
|
118
|
+
this.#drainCrossfadeEvents();
|
|
119
|
+
if (!this.#crossfadeEnabled)
|
|
120
|
+
throw new Error('crossfade is disabled for this session');
|
|
121
|
+
if (!options.entryId?.trim())
|
|
122
|
+
throw new TypeError('entryId is required');
|
|
123
|
+
if (!Number.isSafeInteger(options.queueRevision) || options.queueRevision < 0) {
|
|
124
|
+
throw new RangeError('queueRevision must be a non-negative safe integer');
|
|
125
|
+
}
|
|
126
|
+
const sourceRevision = options.sourceRevision ?? 0;
|
|
127
|
+
if (!Number.isSafeInteger(sourceRevision) || sourceRevision < 0) {
|
|
128
|
+
throw new RangeError('sourceRevision must be a non-negative safe integer');
|
|
129
|
+
}
|
|
130
|
+
if (options.expiresAtMs !== undefined &&
|
|
131
|
+
(!Number.isSafeInteger(options.expiresAtMs) || options.expiresAtMs < 0)) {
|
|
132
|
+
throw new RangeError('expiresAtMs must be a non-negative safe integer');
|
|
133
|
+
}
|
|
134
|
+
const expectedGeneration = options.expectedGeneration ??
|
|
135
|
+
numericGeneration(this.#native.playbackSourceGeneration(this.#playbackId));
|
|
136
|
+
const existing = this.stagedSource;
|
|
137
|
+
if (existing &&
|
|
138
|
+
existing.expectedGeneration === expectedGeneration &&
|
|
139
|
+
existing.entryId === options.entryId &&
|
|
140
|
+
existing.queueRevision === options.queueRevision &&
|
|
141
|
+
existing.sourceRevision === sourceRevision)
|
|
142
|
+
return existing;
|
|
143
|
+
let input;
|
|
144
|
+
const nativeSource = 'stream' in source
|
|
145
|
+
? (input = createReadableInput(this.#native, source.stream)).source
|
|
146
|
+
: toNativeSource(source);
|
|
147
|
+
const key = JSON.stringify([
|
|
148
|
+
expectedGeneration,
|
|
149
|
+
options.entryId,
|
|
150
|
+
options.queueRevision,
|
|
151
|
+
sourceRevision,
|
|
152
|
+
]);
|
|
153
|
+
this.#stages.set(key, { input, metadata: options.customMetadata });
|
|
154
|
+
try {
|
|
155
|
+
const result = await this.#native.playbackStageNext(this.#playbackId, nativeSource, String(expectedGeneration), options.entryId, String(options.queueRevision), String(sourceRevision), options.expiresAtMs === undefined ? undefined : String(options.expiresAtMs));
|
|
156
|
+
this.#drainCrossfadeEvents();
|
|
157
|
+
if (this.#stages.has(key)) {
|
|
158
|
+
this.#stageInput = input;
|
|
159
|
+
this.#stageEntryId = key;
|
|
160
|
+
}
|
|
161
|
+
return this.#normalizeStage(result);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
this.#stages.delete(key);
|
|
165
|
+
await input?.cancel();
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
#normalizeStage(stage) {
|
|
170
|
+
return {
|
|
171
|
+
expectedGeneration: numericGeneration(stage.expectedGeneration),
|
|
172
|
+
entryId: stage.entryId,
|
|
173
|
+
queueRevision: numericGeneration(stage.queueRevision),
|
|
174
|
+
sourceRevision: numericGeneration(stage.sourceRevision),
|
|
175
|
+
state: stage.state,
|
|
176
|
+
bufferedMs: stage.bufferedMs,
|
|
177
|
+
info: stage.info ? toStreamInfo(stage.info) : null,
|
|
178
|
+
error: stage.error ?? null,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
56
181
|
/** Current playback lifecycle state. */
|
|
57
182
|
get state() {
|
|
58
183
|
return this.#native.playbackState(this.#playbackId);
|
|
@@ -153,7 +278,8 @@ export class PlaybackSession extends RemediaEventEmitter {
|
|
|
153
278
|
throw abortError(controller.signal.reason);
|
|
154
279
|
nativeAttempted = true;
|
|
155
280
|
const info = toStreamInfo(await this.#native.play(this.#playbackId, nativeSource));
|
|
156
|
-
const
|
|
281
|
+
const workerGeneration = numericGeneration(this.#native.playbackGeneration(this.#playbackId));
|
|
282
|
+
const generation = numericGeneration(this.#native.playbackSourceGeneration(this.#playbackId));
|
|
157
283
|
this.#latestGeneration = generation;
|
|
158
284
|
this.#metadata.set(generation, options.customMetadata);
|
|
159
285
|
this.#boundMetadata(generation);
|
|
@@ -164,7 +290,9 @@ export class PlaybackSession extends RemediaEventEmitter {
|
|
|
164
290
|
info,
|
|
165
291
|
});
|
|
166
292
|
this.#starting = false;
|
|
167
|
-
|
|
293
|
+
if (this.#crossfadeEnabled)
|
|
294
|
+
this.#startCrossfadePoll();
|
|
295
|
+
void this.#observeGeneration(workerGeneration, input);
|
|
168
296
|
return info;
|
|
169
297
|
}
|
|
170
298
|
catch (error) {
|
|
@@ -232,7 +360,8 @@ export class PlaybackSession extends RemediaEventEmitter {
|
|
|
232
360
|
this.#assertActive();
|
|
233
361
|
const previousPositionMs = this.position.mediaMs;
|
|
234
362
|
await this.#native.seek(this.#playbackId, positionMs);
|
|
235
|
-
|
|
363
|
+
this.#drainCrossfadeEvents();
|
|
364
|
+
if (previousPositionMs !== positionMs || this.#crossfadeEnabled) {
|
|
236
365
|
this.emit(PlaybackEvent.playbackSeeked, {
|
|
237
366
|
...this.#eventContext(),
|
|
238
367
|
positionMs,
|
|
@@ -247,7 +376,7 @@ export class PlaybackSession extends RemediaEventEmitter {
|
|
|
247
376
|
async stop() {
|
|
248
377
|
this.#assertActive();
|
|
249
378
|
const previousState = this.state;
|
|
250
|
-
const generation =
|
|
379
|
+
const generation = this.#latestGeneration;
|
|
251
380
|
try {
|
|
252
381
|
await this.#native.stop(this.#playbackId);
|
|
253
382
|
if (isPlaybackActive(previousState) && this.state === 'stopped') {
|
|
@@ -289,6 +418,9 @@ export class PlaybackSession extends RemediaEventEmitter {
|
|
|
289
418
|
finally {
|
|
290
419
|
await this.#releaseInput();
|
|
291
420
|
this.#disposed = true;
|
|
421
|
+
this.#stopCrossfadePoll();
|
|
422
|
+
await this.#stageInput?.cancel();
|
|
423
|
+
await this.#outgoingInput?.cancel();
|
|
292
424
|
this.hooks.clear();
|
|
293
425
|
this.#metadata.clear();
|
|
294
426
|
this.emit(PlaybackEvent.sessionDisposed, {
|
|
@@ -333,8 +465,9 @@ export class PlaybackSession extends RemediaEventEmitter {
|
|
|
333
465
|
try {
|
|
334
466
|
const state = (await this.#native.wait(this.#playbackId, String(generation)));
|
|
335
467
|
if (state === 'completed' || state === 'failed' || state === 'stopped') {
|
|
468
|
+
this.#drainCrossfadeEvents();
|
|
336
469
|
const error = state === 'failed' ? new Error(this.error ?? 'playback failed') : undefined;
|
|
337
|
-
this.#emitTerminal(state,
|
|
470
|
+
this.#emitTerminal(state, this.#latestGeneration, error);
|
|
338
471
|
}
|
|
339
472
|
}
|
|
340
473
|
catch (error) {
|
|
@@ -351,19 +484,168 @@ export class PlaybackSession extends RemediaEventEmitter {
|
|
|
351
484
|
state = undefined;
|
|
352
485
|
}
|
|
353
486
|
if (state === 'failed')
|
|
354
|
-
this.#emitTerminal('failed',
|
|
487
|
+
this.#emitTerminal('failed', this.#latestGeneration, asError(error));
|
|
355
488
|
}
|
|
356
489
|
}
|
|
357
490
|
finally {
|
|
358
|
-
this.#
|
|
359
|
-
this.#
|
|
360
|
-
|
|
361
|
-
|
|
491
|
+
this.#stopCrossfadePoll();
|
|
492
|
+
this.#clearAbortBinding(this.#playController);
|
|
493
|
+
await this.#outgoingInput?.cancel();
|
|
494
|
+
this.#outgoingInput = undefined;
|
|
495
|
+
await this.#stageInput?.cancel();
|
|
496
|
+
this.#stageInput = undefined;
|
|
497
|
+
for (const stage of this.#stages.values())
|
|
498
|
+
await stage.input?.cancel();
|
|
499
|
+
this.#stages.clear();
|
|
500
|
+
await this.#releaseInput();
|
|
501
|
+
if (input !== this.#input)
|
|
502
|
+
await input?.cancel();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
#startCrossfadePoll() {
|
|
506
|
+
if (this.#crossfadeTimer)
|
|
507
|
+
return;
|
|
508
|
+
this.#crossfadeTimer = setInterval(() => this.#drainCrossfadeEvents(), 25);
|
|
509
|
+
this.#crossfadeTimer.unref?.();
|
|
510
|
+
}
|
|
511
|
+
#stopCrossfadePoll() {
|
|
512
|
+
if (this.#crossfadeTimer)
|
|
513
|
+
clearInterval(this.#crossfadeTimer);
|
|
514
|
+
this.#crossfadeTimer = undefined;
|
|
515
|
+
}
|
|
516
|
+
#drainCrossfadeEvents() {
|
|
517
|
+
if (!this.#crossfadeEnabled || this.#disposed)
|
|
518
|
+
return;
|
|
519
|
+
let events;
|
|
520
|
+
try {
|
|
521
|
+
events = this.#native.playbackTakeCrossfadeEvents(this.#playbackId);
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
for (const event of events) {
|
|
527
|
+
const generation = numericGeneration(event.generation);
|
|
528
|
+
const stage = event.entryId === undefined
|
|
529
|
+
? null
|
|
530
|
+
: {
|
|
531
|
+
entryId: event.entryId,
|
|
532
|
+
queueRevision: numericGeneration(event.queueRevision ?? '0'),
|
|
533
|
+
sourceRevision: numericGeneration(event.sourceRevision ?? '0'),
|
|
534
|
+
};
|
|
535
|
+
const stageKey = JSON.stringify([
|
|
536
|
+
event.kind === 'transition_started'
|
|
537
|
+
? numericGeneration(event.previousGeneration ?? '0')
|
|
538
|
+
: generation,
|
|
539
|
+
stage?.entryId,
|
|
540
|
+
stage?.queueRevision,
|
|
541
|
+
stage?.sourceRevision,
|
|
542
|
+
]);
|
|
543
|
+
switch (event.kind) {
|
|
544
|
+
case 'timeline_changed': {
|
|
545
|
+
const previous = numericGeneration(event.previousGeneration ?? '0');
|
|
546
|
+
this.#metadata.set(generation, this.#metadata.get(previous));
|
|
547
|
+
this.#startedGenerations.add(generation);
|
|
548
|
+
this.#latestGeneration = generation;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
case 'prepare_requested': {
|
|
552
|
+
const remainingMs = event.remainingMs ?? 0;
|
|
553
|
+
this.emit(PlaybackEvent.prepareRequested, {
|
|
554
|
+
...this.#eventContext(generation),
|
|
555
|
+
remainingMs,
|
|
556
|
+
transitionInMs: Math.max(0, remainingMs - this.#fadeDurationMs),
|
|
557
|
+
});
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
case 'stage_ready':
|
|
561
|
+
if (stage)
|
|
562
|
+
this.emit(PlaybackEvent.stageReady, {
|
|
563
|
+
...this.#eventContext(generation),
|
|
564
|
+
...stage,
|
|
565
|
+
});
|
|
566
|
+
break;
|
|
567
|
+
case 'stage_failed':
|
|
568
|
+
void this.#stages.get(stageKey)?.input?.cancel();
|
|
569
|
+
this.#stages.delete(stageKey);
|
|
570
|
+
if (stage)
|
|
571
|
+
this.emit(PlaybackEvent.stageFailed, {
|
|
572
|
+
...this.#eventContext(generation),
|
|
573
|
+
...stage,
|
|
574
|
+
reason: event.reason ?? 'staged source failed',
|
|
575
|
+
});
|
|
576
|
+
break;
|
|
577
|
+
case 'stage_invalidated':
|
|
578
|
+
void this.#stages.get(stageKey)?.input?.cancel();
|
|
579
|
+
this.#stages.delete(stageKey);
|
|
580
|
+
if (stage) {
|
|
581
|
+
this.emit(PlaybackEvent.stageInvalidated, {
|
|
582
|
+
...this.#eventContext(generation),
|
|
583
|
+
...stage,
|
|
584
|
+
});
|
|
585
|
+
if (this.#stageEntryId === stageKey) {
|
|
586
|
+
void this.#stageInput?.cancel();
|
|
587
|
+
this.#stageInput = undefined;
|
|
588
|
+
this.#stageEntryId = undefined;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
break;
|
|
592
|
+
case 'transition_started': {
|
|
593
|
+
if (!stage || !event.info)
|
|
594
|
+
break;
|
|
595
|
+
const previous = numericGeneration(event.previousGeneration ?? '0');
|
|
596
|
+
const prepared = this.#stages.get(stageKey);
|
|
597
|
+
this.#stages.delete(stageKey);
|
|
598
|
+
this.#metadata.set(generation, prepared?.metadata);
|
|
599
|
+
this.#boundMetadata(generation);
|
|
600
|
+
this.#startedGenerations.add(generation);
|
|
601
|
+
this.#latestGeneration = generation;
|
|
602
|
+
this.#outgoingInput = this.#input;
|
|
603
|
+
this.#input = prepared?.input;
|
|
604
|
+
if (this.#stageEntryId === stageKey) {
|
|
605
|
+
this.#stageInput = undefined;
|
|
606
|
+
this.#stageEntryId = undefined;
|
|
607
|
+
}
|
|
608
|
+
this.emit(PlaybackEvent.playbackStarted, {
|
|
609
|
+
...this.#eventContext(generation),
|
|
610
|
+
state: 'playing',
|
|
611
|
+
info: toStreamInfo(event.info),
|
|
612
|
+
transition: { type: 'crossfade', outgoingGeneration: previous, entryId: stage.entryId },
|
|
613
|
+
});
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
case 'transition_completed': {
|
|
617
|
+
if (!stage)
|
|
618
|
+
break;
|
|
619
|
+
const promoted = numericGeneration(event.previousGeneration ?? '0');
|
|
620
|
+
this.#emitTerminal('completed', generation, undefined, undefined, {
|
|
621
|
+
type: 'promoted',
|
|
622
|
+
generation: promoted,
|
|
623
|
+
entryId: stage.entryId,
|
|
624
|
+
});
|
|
625
|
+
void this.#outgoingInput?.cancel();
|
|
626
|
+
this.#outgoingInput = undefined;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
case 'transition_cancelled': {
|
|
630
|
+
if (!stage)
|
|
631
|
+
break;
|
|
632
|
+
const failedGeneration = numericGeneration(event.previousGeneration ?? '0');
|
|
633
|
+
const abandoned = this.#input;
|
|
634
|
+
this.#input = this.#outgoingInput;
|
|
635
|
+
this.#outgoingInput = undefined;
|
|
636
|
+
void abandoned?.cancel();
|
|
637
|
+
this.#latestGeneration = generation;
|
|
638
|
+
this.emit(PlaybackEvent.transitionCancelled, {
|
|
639
|
+
...this.#eventContext(generation),
|
|
640
|
+
failedGeneration,
|
|
641
|
+
entryId: stage.entryId,
|
|
642
|
+
});
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
362
645
|
}
|
|
363
|
-
await this.#releaseInput(input, generation);
|
|
364
646
|
}
|
|
365
647
|
}
|
|
366
|
-
#emitTerminal(state, generation, error, reason) {
|
|
648
|
+
#emitTerminal(state, generation, error, reason, continuation) {
|
|
367
649
|
if ((state !== 'failed' && !this.#startedGenerations.has(generation)) ||
|
|
368
650
|
this.#terminalGenerations.has(generation))
|
|
369
651
|
return;
|
|
@@ -376,7 +658,12 @@ export class PlaybackSession extends RemediaEventEmitter {
|
|
|
376
658
|
}
|
|
377
659
|
const base = this.#eventContext(generation);
|
|
378
660
|
if (state === 'completed') {
|
|
379
|
-
this.emit(PlaybackEvent.playbackCompleted, {
|
|
661
|
+
this.emit(PlaybackEvent.playbackCompleted, {
|
|
662
|
+
...base,
|
|
663
|
+
state,
|
|
664
|
+
position: this.position,
|
|
665
|
+
...(continuation ? { continuation } : {}),
|
|
666
|
+
});
|
|
380
667
|
}
|
|
381
668
|
else if (state === 'stopped') {
|
|
382
669
|
this.emit(PlaybackEvent.playbackStopped, reason ? { ...base, state, reason } : { ...base, state });
|
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 '
|
|
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.
|
|
@@ -172,6 +181,11 @@ function forwardLifecycleEvents(source, target) {
|
|
|
172
181
|
PlaybackEvent.playbackPaused,
|
|
173
182
|
PlaybackEvent.playbackResumed,
|
|
174
183
|
PlaybackEvent.playbackSeeked,
|
|
184
|
+
PlaybackEvent.prepareRequested,
|
|
185
|
+
PlaybackEvent.stageReady,
|
|
186
|
+
PlaybackEvent.stageFailed,
|
|
187
|
+
PlaybackEvent.stageInvalidated,
|
|
188
|
+
PlaybackEvent.transitionCancelled,
|
|
175
189
|
PlaybackEvent.filterChanged,
|
|
176
190
|
PlaybackEvent.volumeChanged,
|
|
177
191
|
PlaybackEvent.sessionDisposed,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Native process memory accounting in bytes. Missing fields indicate an older server; null indicates unavailable data. */
|
|
2
|
+
export interface ResourceMemory {
|
|
3
|
+
/** Resident bytes including shared pages. */
|
|
4
|
+
residentBytes?: number | null;
|
|
5
|
+
/** Private resident bytes (USS); excludes shared pages, distinct from private commit. */
|
|
6
|
+
privateResidentBytes?: number | null;
|
|
7
|
+
/** Proportional resident bytes (PSS), including shared pages proportionally. */
|
|
8
|
+
proportionalResidentBytes?: number | null;
|
|
9
|
+
/** Private committed bytes, including potentially nonresident pages. */
|
|
10
|
+
committedPrivateBytes?: number | null;
|
|
11
|
+
/** Mapped virtual address bytes; not physical RAM. */
|
|
12
|
+
virtualBytes?: number | null;
|
|
13
|
+
/** Reserved but uncommitted virtual address bytes; not RAM. */
|
|
14
|
+
reservedVirtualBytes?: number | null;
|
|
15
|
+
/** Lifetime peak resident bytes. */
|
|
16
|
+
peakResidentBytes?: number | null;
|
|
17
|
+
/** macOS physical footprint bytes; distinct from USS and RSS. */
|
|
18
|
+
footprintBytes?: number | null;
|
|
19
|
+
}
|
|
20
|
+
/** Native process CPU accounting. Missing fields indicate an older server; null indicates unavailable data. */
|
|
21
|
+
export interface ResourceCpu {
|
|
22
|
+
/** Process utilization: 100 means one logical CPU; can exceed 100. Null on the first sample. */
|
|
23
|
+
processPercent?: number | null;
|
|
24
|
+
/** Cumulative process user CPU time in milliseconds. */
|
|
25
|
+
userTimeMs?: number | null;
|
|
26
|
+
/** Cumulative process system CPU time in milliseconds. */
|
|
27
|
+
systemTimeMs?: number | null;
|
|
28
|
+
/** Available logical CPU parallelism estimate. */
|
|
29
|
+
availableParallelism?: number | null;
|
|
30
|
+
/** Actual process OS thread count, including nonaudio threads. */
|
|
31
|
+
threadCount?: number | null;
|
|
32
|
+
/** System one-minute load average; not process CPU usage. */
|
|
33
|
+
loadAverage1m?: number | null;
|
|
34
|
+
}
|
|
35
|
+
/** Cached native measurements for the whole host process, shared by all embedded instances. */
|
|
36
|
+
export interface ProcessResourceStatistics {
|
|
37
|
+
/** Process memory counters in bytes. */
|
|
38
|
+
memory: ResourceMemory;
|
|
39
|
+
/** Process CPU and system load counters. */
|
|
40
|
+
cpu: ResourceCpu;
|
|
41
|
+
/** Collection completion timestamp as Unix milliseconds; null before the first collection. */
|
|
42
|
+
collectedAtUnixMs: number | null;
|
|
43
|
+
/** Wall interval used for process utilization in milliseconds; null on the first sample. */
|
|
44
|
+
cpuSampleIntervalMs: number | null;
|
|
45
|
+
/** Demand-driven cache refresh interval in milliseconds; concurrent requests share stale snapshots. */
|
|
46
|
+
samplingIntervalMs: number;
|
|
47
|
+
/** Native collection duration in milliseconds, or null before collection. */
|
|
48
|
+
collectionDurationMs: number | null;
|
|
49
|
+
/** Unix descriptors or Windows handles owned by the process, or null if unavailable. */
|
|
50
|
+
openHandles: number | null;
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|