@remedia/node 0.1.0-dev.0 → 0.1.0-dev.2

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/dist/lifecycle.js CHANGED
@@ -1,28 +1,51 @@
1
1
  /** Stable names for playback/session event subscriptions and emission. */
2
2
  export const PlaybackSessionEvents = {
3
+ /** Emitted after a source starts and its stream information is available. */
3
4
  playbackStarted: 'playbackStarted',
5
+ /** Emitted when a source generation reaches natural end-of-stream. */
4
6
  playbackCompleted: 'playbackCompleted',
7
+ /** Emitted when a source generation is explicitly stopped or cancelled. */
5
8
  playbackStopped: 'playbackStopped',
9
+ /** Emitted when a source generation fails to open, decode, process, or deliver output. */
6
10
  playbackFailed: 'playbackFailed',
11
+ /** Emitted when playback is paused without replacing its source generation. */
7
12
  playbackPaused: 'playbackPaused',
13
+ /** Emitted when paused playback resumes. */
8
14
  playbackResumed: 'playbackResumed',
15
+ /** Emitted after seeking to a source timeline position. */
9
16
  playbackSeeked: 'playbackSeeked',
17
+ /** Emitted when the configured session filtergraph changes. */
10
18
  filterChanged: 'filterChanged',
19
+ /** Emitted when the configured session linear gain changes. */
11
20
  volumeChanged: 'volumeChanged',
21
+ /** Emitted when a session is unregistered and its owned resources are released. */
12
22
  sessionDisposed: 'sessionDisposed',
13
23
  };
24
+ /** Stable event names for typed VoiceSessionEvents subscriptions. */
14
25
  export const VoiceSessionEvents = {
26
+ /** Emitted when the Discord transport lifecycle state changes. */
15
27
  voiceStateChanged: 'voiceStateChanged',
28
+ /** Emitted when voice connection or recovery fails. */
16
29
  voiceFailed: 'voiceFailed',
17
30
  };
31
+ /** Stable event names for typed RemediaEvents subscriptions. */
18
32
  export const RemediaEvents = {
19
33
  ...PlaybackSessionEvents,
20
34
  ...VoiceSessionEvents,
35
+ /** Emitted for a structured native diagnostic event. */
21
36
  debug: 'debug',
22
37
  };
23
- /** Ordered, awaited local middleware. It is not a server authorization mechanism. */
38
+ /**
39
+ * Ordered, awaited local middleware. It is not a server authorization mechanism.
40
+ * @typeParam Metadata - Application-owned metadata attached to playback generations.
41
+ */
24
42
  export class PlaybackHooks {
25
43
  #beforePlay = [];
44
+ /**
45
+ * Append an awaited pre-play hook. Hooks run in registration order before a source opens.
46
+ * @param hook - Local callback to await before opening each source; returning cancel rejects playback.
47
+ * @returns An idempotent function that removes this hook registration.
48
+ */
26
49
  beforePlay(hook) {
27
50
  if (typeof hook !== 'function')
28
51
  throw new TypeError('beforePlay hook must be a function');
@@ -33,6 +56,11 @@ export class PlaybackHooks {
33
56
  this.#beforePlay.splice(index, 1);
34
57
  };
35
58
  }
59
+ /**
60
+ * Run registered pre-play hooks in order and reject on cancellation or hook failure.
61
+ * @param context - Source, local metadata, and cancellation signal supplied to the awaited hooks.
62
+ * @returns Resolves when every hook permits playback; cancellation rejects with AbortError.
63
+ */
36
64
  async runBeforePlay(context) {
37
65
  for (const hook of [...this.#beforePlay]) {
38
66
  if (context.signal.aborted)
@@ -52,6 +80,14 @@ export class PlaybackHooks {
52
80
  this.#beforePlay.length = 0;
53
81
  }
54
82
  }
83
+ /**
84
+ * Create playback event correlation fields for one local source generation.
85
+ * @param generation - Native source generation associated with the lifecycle notification.
86
+ * @param customMetadata - Application-owned metadata associated with this source generation.
87
+ * @returns Identity fields with metadata included only when provided.
88
+ * @typeParam Metadata - Application-owned metadata attached to playback generations.
89
+ * @param sessionId - Opaque Remedia session identity used for event correlation.
90
+ */
55
91
  export function lifecycleContext(sessionId, generation, customMetadata) {
56
92
  return customMetadata === undefined
57
93
  ? { sessionId, nodeId: null, generation }
@@ -3,27 +3,71 @@ import { type MediaSource } from './input.js';
3
3
  import { PlaybackHooks, type PlayOptions, type PlaybackSessionEventMap } from './lifecycle.js';
4
4
  import { type AudioStreamInfo, type PlaybackPosition, type PlaybackState } from './types.js';
5
5
  import { FilterControls } from './filters.js';
6
- /** Controls one playback session. Local sessions decode into a no-op output. */
6
+ /**
7
+ * Controls one playback session. Local sessions decode into a no-op output.
8
+ * @typeParam Metadata - Application-owned metadata attached to playback generations.
9
+ */
7
10
  export declare class PlaybackSession<Metadata = unknown> extends RemediaEventEmitter<PlaybackSessionEventMap<Metadata>> {
8
11
  #private;
12
+ /** Ordered, awaited local pre-play hooks; listeners are separate from hooks. */
9
13
  readonly hooks: PlaybackHooks<Metadata>;
14
+ /** Persistent raw FFmpeg filtergraph controls for this session. */
10
15
  readonly filters: FilterControls;
11
16
  /** Stable opaque identity for this wrapper, suitable for correlating lifecycle events. */
12
17
  get sessionId(): string;
18
+ /** Current playback lifecycle state. */
13
19
  get state(): PlaybackState;
20
+ /** Latest source and output timeline measurements in milliseconds. */
14
21
  get position(): PlaybackPosition;
22
+ /** Source duration in milliseconds, or null when unknown. */
15
23
  get durationMs(): number | null;
24
+ /** Most recent playback or voice failure message, or null when no error is available. */
16
25
  get error(): string | null;
17
26
  /** Current normalized linear master gain, from 0.0 (mute) through 2.0. */
18
27
  get volume(): number;
19
- /** Set master gain independently of the raw filtergraph. */
28
+ /**
29
+ * Set master gain independently of the raw filtergraph.
30
+ * @param gain - Finite linear master gain from 0 through 2; 1 leaves amplitude unchanged.
31
+ * @returns Resolves after the gain is stored or applied; rejects for non-finite gain or values outside 0 through 2.
32
+ * @throws {RangeError} If gain is non-finite or outside the inclusive range 0 through 2.
33
+ */
20
34
  setVolume(gain: number): Promise<void>;
35
+ /**
36
+ * Open one media source and begin a new playback generation using the persistent session configuration. Stop active playback before starting another source.
37
+ * @param source - Encoded media input; Buffer and caller-owned binary Readable inputs require embedded mode.
38
+ * @param options - Optional metadata for this generation and a cancellation signal.
39
+ * @returns Selected stream metadata once startup succeeds; rejects if hooks, input opening, or playback startup fail.
40
+ */
21
41
  play(source: MediaSource, options?: PlayOptions<Metadata>): Promise<AudioStreamInfo>;
42
+ /**
43
+ * Pause the current playback without replacing its decoder or source generation.
44
+ * @returns Resolves when the backend accepts the pause request.
45
+ */
22
46
  pause(): Promise<void>;
47
+ /**
48
+ * Resume paused playback on its existing source generation.
49
+ * @returns Resolves when the backend accepts the resume request.
50
+ */
23
51
  resume(): Promise<void>;
52
+ /**
53
+ * Seek on the original media timeline; non-seekable and one-shot sources may reject.
54
+ * @param positionMs - Non-negative finite position on the original media timeline, in milliseconds.
55
+ * @returns Resolves after the backend accepts the seek; rejects for invalid positions or unsupported inputs.
56
+ */
24
57
  seek(positionMs: number): Promise<void>;
58
+ /**
59
+ * Stop the active playback generation and release its input without leaving the voice channel.
60
+ * @returns Resolves after the backend stops the active source.
61
+ */
25
62
  stop(): Promise<void>;
63
+ /**
64
+ * Wait for the current playback generation to reach a terminal state.
65
+ * @returns The terminal playback state for the generation captured by this wait.
66
+ */
26
67
  wait(): Promise<PlaybackState>;
27
- /** Stop and release this standalone playback session. */
68
+ /**
69
+ * Stop and release this standalone playback session.
70
+ * @returns Resolves after disposal; rejects if resource cleanup fails.
71
+ */
28
72
  dispose(): Promise<void>;
29
73
  }
package/dist/playback.js CHANGED
@@ -3,7 +3,10 @@ import { createReadableInput, toNativeSource, } from './input.js';
3
3
  import { lifecycleContext, PlaybackHooks, PlaybackSessionEvents as PlaybackEvent, } from './lifecycle.js';
4
4
  import { toStreamInfo, } from './types.js';
5
5
  import { FilterControls, validateVolume } from './filters.js';
6
- /** Controls one playback session. Local sessions decode into a no-op output. */
6
+ /**
7
+ * Controls one playback session. Local sessions decode into a no-op output.
8
+ * @typeParam Metadata - Application-owned metadata attached to playback generations.
9
+ */
7
10
  export class PlaybackSession extends RemediaEventEmitter {
8
11
  #native;
9
12
  #playbackId;
@@ -11,7 +14,9 @@ export class PlaybackSession extends RemediaEventEmitter {
11
14
  #terminalGenerations = new Set();
12
15
  #metadata = new Map();
13
16
  #startedGenerations = new Set();
17
+ /** Ordered, awaited local pre-play hooks; listeners are separate from hooks. */
14
18
  hooks;
19
+ /** Persistent raw FFmpeg filtergraph controls for this session. */
15
20
  filters;
16
21
  #input;
17
22
  #disposed = false;
@@ -21,7 +26,16 @@ export class PlaybackSession extends RemediaEventEmitter {
21
26
  #abortListener;
22
27
  #latestGeneration = 0;
23
28
  #guildId;
24
- /** @internal */
29
+ /**
30
+ * Construct this wrapper with its owning services. Use the owning engine or client factory for application code.
31
+ *
32
+ * @internal
33
+ * @param native - Owning native engine instance; its resources must outlive this wrapper.
34
+ * @param playbackId - Opaque native playback identity owned by this wrapper.
35
+ * @param onDispose - Callback invoked when this wrapper releases its owning registration.
36
+ * @param guildId - Discord guild snowflake used to route the operation.
37
+ * @param hooks - Optional shared pre-play hooks for the owning Discord session.
38
+ */
25
39
  constructor(native, playbackId, onDispose, guildId, hooks) {
26
40
  super();
27
41
  this.#native = native;
@@ -39,9 +53,11 @@ export class PlaybackSession extends RemediaEventEmitter {
39
53
  get currentGeneration() {
40
54
  return this.#latestGeneration;
41
55
  }
56
+ /** Current playback lifecycle state. */
42
57
  get state() {
43
58
  return this.#native.playbackState(this.#playbackId);
44
59
  }
60
+ /** Latest source and output timeline measurements in milliseconds. */
45
61
  get position() {
46
62
  const position = this.#native.playbackPosition(this.#playbackId);
47
63
  return {
@@ -54,9 +70,11 @@ export class PlaybackSession extends RemediaEventEmitter {
54
70
  playedMs: position.playedMs ?? null,
55
71
  };
56
72
  }
73
+ /** Source duration in milliseconds, or null when unknown. */
57
74
  get durationMs() {
58
75
  return this.#native.playbackDurationMs(this.#playbackId) ?? null;
59
76
  }
77
+ /** Most recent playback or voice failure message, or null when no error is available. */
60
78
  get error() {
61
79
  return this.#native.playbackError(this.#playbackId) ?? null;
62
80
  }
@@ -64,7 +82,12 @@ export class PlaybackSession extends RemediaEventEmitter {
64
82
  get volume() {
65
83
  return this.#native.playbackVolume(this.#playbackId);
66
84
  }
67
- /** Set master gain independently of the raw filtergraph. */
85
+ /**
86
+ * Set master gain independently of the raw filtergraph.
87
+ * @param gain - Finite linear master gain from 0 through 2; 1 leaves amplitude unchanged.
88
+ * @returns Resolves after the gain is stored or applied; rejects for non-finite gain or values outside 0 through 2.
89
+ * @throws {RangeError} If gain is non-finite or outside the inclusive range 0 through 2.
90
+ */
68
91
  async setVolume(gain) {
69
92
  this.#assertActive();
70
93
  validateVolume(gain);
@@ -78,6 +101,12 @@ export class PlaybackSession extends RemediaEventEmitter {
78
101
  volume: gain,
79
102
  });
80
103
  }
104
+ /**
105
+ * Open one media source and begin a new playback generation using the persistent session configuration. Stop active playback before starting another source.
106
+ * @param source - Encoded media input; Buffer and caller-owned binary Readable inputs require embedded mode.
107
+ * @param options - Optional metadata for this generation and a cancellation signal.
108
+ * @returns Selected stream metadata once startup succeeds; rejects if hooks, input opening, or playback startup fail.
109
+ */
81
110
  async play(source, options = {}) {
82
111
  this.#assertActive();
83
112
  if (this.#starting || isPlaybackActive(this.state)) {
@@ -160,6 +189,10 @@ export class PlaybackSession extends RemediaEventEmitter {
160
189
  }
161
190
  }
162
191
  }
192
+ /**
193
+ * Pause the current playback without replacing its decoder or source generation.
194
+ * @returns Resolves when the backend accepts the pause request.
195
+ */
163
196
  async pause() {
164
197
  this.#assertActive();
165
198
  const previousState = this.state;
@@ -172,6 +205,10 @@ export class PlaybackSession extends RemediaEventEmitter {
172
205
  });
173
206
  }
174
207
  }
208
+ /**
209
+ * Resume paused playback on its existing source generation.
210
+ * @returns Resolves when the backend accepts the resume request.
211
+ */
175
212
  async resume() {
176
213
  this.#assertActive();
177
214
  const previousState = this.state;
@@ -184,6 +221,11 @@ export class PlaybackSession extends RemediaEventEmitter {
184
221
  });
185
222
  }
186
223
  }
224
+ /**
225
+ * Seek on the original media timeline; non-seekable and one-shot sources may reject.
226
+ * @param positionMs - Non-negative finite position on the original media timeline, in milliseconds.
227
+ * @returns Resolves after the backend accepts the seek; rejects for invalid positions or unsupported inputs.
228
+ */
187
229
  async seek(positionMs) {
188
230
  this.#assertActive();
189
231
  const previousPositionMs = this.position.mediaMs;
@@ -196,6 +238,10 @@ export class PlaybackSession extends RemediaEventEmitter {
196
238
  });
197
239
  }
198
240
  }
241
+ /**
242
+ * Stop the active playback generation and release its input without leaving the voice channel.
243
+ * @returns Resolves after the backend stops the active source.
244
+ */
199
245
  async stop() {
200
246
  this.#assertActive();
201
247
  const previousState = this.state;
@@ -210,6 +256,10 @@ export class PlaybackSession extends RemediaEventEmitter {
210
256
  await this.#releaseInput();
211
257
  }
212
258
  }
259
+ /**
260
+ * Wait for the current playback generation to reach a terminal state.
261
+ * @returns The terminal playback state for the generation captured by this wait.
262
+ */
213
263
  async wait() {
214
264
  this.#assertActive();
215
265
  // Capture synchronously so the native blocking waiter stays bound to the source
@@ -222,7 +272,10 @@ export class PlaybackSession extends RemediaEventEmitter {
222
272
  await this.#releaseInput();
223
273
  }
224
274
  }
225
- /** Stop and release this standalone playback session. */
275
+ /**
276
+ * Stop and release this standalone playback session.
277
+ * @returns Resolves after disposal; rejects if resource cleanup fails.
278
+ */
226
279
  async dispose() {
227
280
  if (this.#disposed)
228
281
  return;
@@ -245,7 +298,12 @@ export class PlaybackSession extends RemediaEventEmitter {
245
298
  this.#onDispose?.();
246
299
  }
247
300
  }
248
- /** @internal */
301
+ /**
302
+ * Detach and cancel this wrapper's bounded input pump without destroying the caller-owned Readable.
303
+ *
304
+ * @internal
305
+ * @returns Resolves after the input pump has released its listeners and native queue.
306
+ */
249
307
  async releaseInput() {
250
308
  await this.#releaseInput();
251
309
  }
package/dist/remedia.d.ts CHANGED
@@ -1,13 +1,23 @@
1
+ import { type MediaSource } from './input.js';
2
+ import { type AudioStreamInfo } from './types.js';
1
3
  import { RemediaEventEmitter } from './events.js';
2
4
  import { DiscordSession, type DiscordSessionOptions } from './discord.js';
3
5
  import { type DiscordGatewaySend } from './gateway.js';
4
6
  import type { RemediaLifecycleEvents } from './lifecycle.js';
5
7
  import { PlaybackSession } from './playback.js';
6
8
  import { type DebugListener, type DiagnosticEvent } from './types.js';
9
+ /** Optional Gateway integration for the embedded engine; required when creating Discord sessions. */
7
10
  export interface RemediaOptions {
8
11
  /** Send raw Gateway payloads through the application's existing Discord client or shard. */
9
12
  send: DiscordGatewaySend;
10
13
  }
14
+ /** Cancellation and deadline for disposable metadata inspection. */
15
+ export interface ProbeOptions {
16
+ /** Cancels input I/O and releases probe resources; never destroys caller-owned Readables. */
17
+ signal?: AbortSignal;
18
+ /** Total deadline in milliseconds, from 1 through 4294967295; defaults to 15000. */
19
+ timeoutMs?: number;
20
+ }
11
21
  /**
12
22
  * One embedded Remedia context. Create instances with `await Remedia.create()` so native
13
23
  * observability and optional asynchronous OpenTelemetry exporters initialize on NAPI-RS's runtime.
@@ -15,18 +25,43 @@ export interface RemediaOptions {
15
25
  export declare class Remedia extends RemediaEventEmitter<RemediaLifecycleEvents> {
16
26
  #private;
17
27
  private constructor();
18
- /** Create an independent native context using REMEDIA_* and OTEL_* environment settings. */
28
+ /**
29
+ * Create an independent native context using REMEDIA_* and OTEL_* environment settings.
30
+ * @param options - Optional bot Gateway callback; required when creating Discord sessions.
31
+ * @returns An initialized independent native engine context.
32
+ */
19
33
  static create(options?: RemediaOptions): Promise<Remedia>;
34
+ /**
35
+ * Inspect a disposable source without creating playback or voice resources. Readables may be partially or fully consumed.
36
+ * @param source - Encoded input; acquire a fresh source for subsequent playback.
37
+ * @param options - Cancellation and total deadline; defaults to 15000 ms.
38
+ * @returns Available stream metadata, with unknown numeric fields null; rejects invalid media, cancellation, timeout, or capacity exhaustion.
39
+ */
40
+ probe(source: MediaSource, options?: ProbeOptions): Promise<AudioStreamInfo>;
20
41
  /** Number of diagnostic events dropped by either bounded event queue. */
21
42
  get droppedDebugEvents(): number;
22
- /** Register a structured native diagnostic listener. */
23
- /** Create a local decoder session that discards its PCM output. */
43
+ /**
44
+ * Create a local decoder session that discards its PCM output.
45
+ * @returns An independent playback session owned by this engine until disposal.
46
+ * @typeParam Metadata - Application-owned metadata attached to playback generations.
47
+ */
24
48
  createPlaybackSession<Metadata = unknown>(): PlaybackSession<Metadata>;
25
- /** Create a Gateway-managed voice session. Call `connect()` to send OP 4 and join the channel. */
49
+ /**
50
+ * Create a Gateway-managed voice session. Call `connect()` to send OP 4 and join the channel.
51
+ * @param options - Guild and bot identity, requested channel, initial processing settings, and voice flags.
52
+ * @returns The unconnected session; call connect() to request a Gateway voice join.
53
+ * @typeParam Metadata - Application-owned metadata attached to playback generations.
54
+ */
26
55
  createDiscordSession<Metadata = unknown>(options: DiscordSessionOptions): DiscordSession<Metadata>;
27
- /** Forward a raw Discord Gateway packet to the matching in-process voice session. */
56
+ /**
57
+ * Forward a raw Discord Gateway packet to the matching in-process voice session.
58
+ * @param packet - Raw packet from this bot's Gateway dispatch stream.
59
+ */
28
60
  receive(packet: unknown): void;
29
- /** Stop all playbacks, disconnect transports, flush telemetry, and release native resources. */
61
+ /**
62
+ * Stop all playbacks, disconnect transports, flush telemetry, and release native resources.
63
+ * @returns Resolves after cleanup; rejects when a resource fails to close.
64
+ */
30
65
  close(): Promise<void>;
31
66
  }
32
67
  export type { DebugListener, DiagnosticEvent };
package/dist/remedia.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { createReadableInput, toNativeSource } from './input.js';
2
+ import { toStreamInfo } from './types.js';
1
3
  import { RemediaEventEmitter } from './events.js';
2
4
  import { NativeRemedia } from '../native.js';
3
5
  import { DiscordSession } from './discord.js';
@@ -11,6 +13,7 @@ import { toDiagnosticEvent } from './types.js';
11
13
  */
12
14
  export class Remedia extends RemediaEventEmitter {
13
15
  #native;
16
+ #probes = new Set();
14
17
  #sessions = new Set();
15
18
  #discordSessions = new Map();
16
19
  #gateway;
@@ -24,16 +27,64 @@ export class Remedia extends RemediaEventEmitter {
24
27
  this.emit(RemediaEvent.debug, diagnostic);
25
28
  });
26
29
  }
27
- /** Create an independent native context using REMEDIA_* and OTEL_* environment settings. */
30
+ /**
31
+ * Create an independent native context using REMEDIA_* and OTEL_* environment settings.
32
+ * @param options - Optional bot Gateway callback; required when creating Discord sessions.
33
+ * @returns An initialized independent native engine context.
34
+ */
28
35
  static async create(options) {
29
36
  return new Remedia(await NativeRemedia.create(), options);
30
37
  }
38
+ /**
39
+ * Inspect a disposable source without creating playback or voice resources. Readables may be partially or fully consumed.
40
+ * @param source - Encoded input; acquire a fresh source for subsequent playback.
41
+ * @param options - Cancellation and total deadline; defaults to 15000 ms.
42
+ * @returns Available stream metadata, with unknown numeric fields null; rejects invalid media, cancellation, timeout, or capacity exhaustion.
43
+ */
44
+ async probe(source, options = {}) {
45
+ this.#assertOpen();
46
+ const timeoutMs = options.timeoutMs ?? 15_000;
47
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 0xffffffff)
48
+ throw new RangeError('invalid probe timeoutMs');
49
+ options.signal?.throwIfAborted();
50
+ const id = this.#native.createProbe();
51
+ const controller = new AbortController();
52
+ this.#probes.add(controller);
53
+ const abort = () => {
54
+ controller.abort();
55
+ this.#native.cancelProbe(id);
56
+ };
57
+ options.signal?.addEventListener('abort', abort, { once: true });
58
+ controller.signal.addEventListener('abort', () => this.#native.cancelProbe(id), { once: true });
59
+ let input;
60
+ try {
61
+ const nativeSource = 'stream' in source
62
+ ? (input = createReadableInput(this.#native, source.stream)).source
63
+ : toNativeSource(source);
64
+ const info = await this.#native.probe(id, nativeSource, timeoutMs);
65
+ controller.signal.throwIfAborted();
66
+ return toStreamInfo(info);
67
+ }
68
+ catch (error) {
69
+ controller.signal.throwIfAborted();
70
+ throw error;
71
+ }
72
+ finally {
73
+ options.signal?.removeEventListener('abort', abort);
74
+ this.#native.cancelProbe(id);
75
+ await input?.cancel();
76
+ this.#probes.delete(controller);
77
+ }
78
+ }
31
79
  /** Number of diagnostic events dropped by either bounded event queue. */
32
80
  get droppedDebugEvents() {
33
81
  return this.#native.droppedDebugEvents;
34
82
  }
35
- /** Register a structured native diagnostic listener. */
36
- /** Create a local decoder session that discards its PCM output. */
83
+ /**
84
+ * Create a local decoder session that discards its PCM output.
85
+ * @returns An independent playback session owned by this engine until disposal.
86
+ * @typeParam Metadata - Application-owned metadata attached to playback generations.
87
+ */
37
88
  createPlaybackSession() {
38
89
  this.#assertOpen();
39
90
  let playback;
@@ -44,7 +95,12 @@ export class Remedia extends RemediaEventEmitter {
44
95
  this.#sessions.add(playback);
45
96
  return playback;
46
97
  }
47
- /** Create a Gateway-managed voice session. Call `connect()` to send OP 4 and join the channel. */
98
+ /**
99
+ * Create a Gateway-managed voice session. Call `connect()` to send OP 4 and join the channel.
100
+ * @param options - Guild and bot identity, requested channel, initial processing settings, and voice flags.
101
+ * @returns The unconnected session; call connect() to request a Gateway voice join.
102
+ * @typeParam Metadata - Application-owned metadata attached to playback generations.
103
+ */
48
104
  createDiscordSession(options) {
49
105
  this.#assertOpen();
50
106
  if (!this.#gateway) {
@@ -61,7 +117,10 @@ export class Remedia extends RemediaEventEmitter {
61
117
  this.#discordSessions.set(session.guildId, session);
62
118
  return session;
63
119
  }
64
- /** Forward a raw Discord Gateway packet to the matching in-process voice session. */
120
+ /**
121
+ * Forward a raw Discord Gateway packet to the matching in-process voice session.
122
+ * @param packet - Raw packet from this bot's Gateway dispatch stream.
123
+ */
65
124
  receive(packet) {
66
125
  if (this.#closed || !this.#gateway)
67
126
  return;
@@ -72,11 +131,16 @@ export class Remedia extends RemediaEventEmitter {
72
131
  .get(dispatch.guildId)
73
132
  ?.receiveGatewayDispatch(dispatch.event, dispatch.data, dispatch.sequence, dispatch.epoch);
74
133
  }
75
- /** Stop all playbacks, disconnect transports, flush telemetry, and release native resources. */
134
+ /**
135
+ * Stop all playbacks, disconnect transports, flush telemetry, and release native resources.
136
+ * @returns Resolves after cleanup; rejects when a resource fails to close.
137
+ */
76
138
  async close() {
77
139
  if (this.#closed)
78
140
  return;
79
141
  this.#closed = true;
142
+ for (const controller of this.#probes)
143
+ controller.abort();
80
144
  const voiceCleanup = await Promise.allSettled([...this.#discordSessions.values()].map((session) => session.dispose()));
81
145
  try {
82
146
  await Promise.all([...this.#sessions].map((session) => session.dispose()));
package/dist/types.d.ts CHANGED
@@ -1,48 +1,97 @@
1
1
  import type { NativeDiagnosticEvent, NativeStreamInfo } from '../native.js';
2
+ /** Lifecycle state of a playback pipeline, independent of its voice transport state. */
2
3
  export type PlaybackState = 'idle' | 'starting' | 'playing' | 'pausing' | 'resuming' | 'paused' | 'stopping' | 'stopped' | 'completed' | 'failed';
4
+ /** Discord transport lifecycle state, including credential negotiation and recovery. */
3
5
  export type VoiceConnectionState = 'connecting' | 'negotiatingDave' | 'connected' | 'reconnecting' | 'disconnecting' | 'disconnected' | 'failed';
6
+ /** Metadata and format information for the selected audio stream; unavailable measurements are null. */
4
7
  export interface AudioStreamInfo {
8
+ /** Zero-based index of the selected audio stream in the media container. */
5
9
  streamIndex: number;
10
+ /** Codec name reported by the selected audio stream. */
6
11
  codec: string;
12
+ /** Encoded stream sample rate in Hz, or null when unknown; playback output is normalized separately. */
7
13
  sampleRate: number | null;
14
+ /** Encoded stream channel count, or null when unknown. */
8
15
  channels: number | null;
16
+ /** Encoded stream channel layout description, or null when unknown. */
9
17
  channelLayout: string | null;
18
+ /** Source duration in milliseconds, or null when unknown. */
10
19
  durationMs: number | null;
20
+ /** Stream bit rate in bits per second, or null when unknown. */
11
21
  bitRate: number | null;
22
+ /** Container or stream metadata keyed by native tag name. */
12
23
  metadata: Readonly<Record<string, string>>;
13
24
  }
25
+ /** Source and output timeline measurements in milliseconds; unsupported measurements are null. */
14
26
  export interface PlaybackPosition {
15
27
  /** Original media timeline associated with the latest submitted PCM block. */
16
28
  mediaMs: number | null;
17
29
  /** `timestamp` when derived from source PTS, `estimated` when inferred, or null. */
18
30
  mediaPositionAccuracy: 'timestamp' | 'estimated' | null;
31
+ /**
32
+ * Duration of audio submitted to the output in milliseconds; may differ from source position after filtering.
33
+ */
19
34
  submittedMs: number;
35
+ /** Output-reported played duration in milliseconds, or null when the output does not provide it. */
20
36
  playedMs: number | null;
21
37
  }
38
+ /** Severity of a structured native tracing event. */
22
39
  export type DiagnosticSeverity = 'trace' | 'debug' | 'info' | 'warn' | 'error';
40
+ /** Typed diagnostic value; integers remain decimal strings to preserve precision in JavaScript. */
23
41
  export type DiagnosticFieldValue = {
42
+ /** Discriminant describing the representation of the diagnostic field. */
24
43
  kind: 'string' | 'debug';
44
+ /** Diagnostic field value in the representation selected by kind. */
25
45
  value: string;
26
46
  } | {
47
+ /** Discriminant describing the representation of the diagnostic field. */
27
48
  kind: 'integer';
49
+ /** Diagnostic field value in the representation selected by kind. */
28
50
  value: string;
29
51
  } | {
52
+ /** Discriminant describing the representation of the diagnostic field. */
30
53
  kind: 'boolean';
54
+ /** Diagnostic field value in the representation selected by kind. */
31
55
  value: boolean;
32
56
  } | {
57
+ /** Discriminant describing the representation of the diagnostic field. */
33
58
  kind: 'number';
59
+ /** Diagnostic field value in the representation selected by kind. */
34
60
  value: number;
35
61
  };
62
+ /** Structured native diagnostic event forwarded through the bounded diagnostic queues. */
36
63
  export interface DiagnosticEvent {
64
+ /** Unix timestamp of the diagnostic event in milliseconds. */
37
65
  timestamp: number;
66
+ /** Native tracing severity associated with the event. */
38
67
  severity: DiagnosticSeverity;
68
+ /** Native tracing target or module that emitted the diagnostic. */
39
69
  target: string;
70
+ /** Server or diagnostic event name. */
40
71
  event: string;
72
+ /** Human-readable diagnostic or failure description. */
41
73
  message: string | null;
74
+ /** Native playback identity, or null for diagnostics without playback context. */
42
75
  playbackId: string | null;
76
+ /** Native voice connection identity, or null for diagnostics without voice context. */
43
77
  voiceConnectionId: string | null;
78
+ /** Typed structured diagnostic fields; integers are represented as decimal strings. */
44
79
  fields: Readonly<Record<string, DiagnosticFieldValue>>;
45
80
  }
81
+ /**
82
+ * Callback receiving a structured native diagnostic event.
83
+ * @param event - Structured native diagnostic delivered by the bounded event queue.
84
+ */
46
85
  export type DebugListener = (event: DiagnosticEvent) => void;
86
+ /**
87
+ * Normalize native optional stream metadata into explicit nullable public fields.
88
+ * @param info - Native stream metadata to normalize into the public nullable representation.
89
+ * @returns Stream metadata using the public AudioStreamInfo representation.
90
+ */
47
91
  export declare function toStreamInfo(info: NativeStreamInfo): AudioStreamInfo;
92
+ /**
93
+ * Normalize a structured native diagnostic; invalid severity or field representations throw.
94
+ * @param event - Event name identifying the typed notification or Gateway voice dispatch.
95
+ * @returns A diagnostic event with typed field values.
96
+ */
48
97
  export declare function toDiagnosticEvent(event: NativeDiagnosticEvent): DiagnosticEvent;
package/dist/types.js CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Normalize native optional stream metadata into explicit nullable public fields.
3
+ * @param info - Native stream metadata to normalize into the public nullable representation.
4
+ * @returns Stream metadata using the public AudioStreamInfo representation.
5
+ */
1
6
  export function toStreamInfo(info) {
2
7
  return {
3
8
  streamIndex: info.streamIndex,
@@ -10,6 +15,11 @@ export function toStreamInfo(info) {
10
15
  metadata: info.metadata,
11
16
  };
12
17
  }
18
+ /**
19
+ * Normalize a structured native diagnostic; invalid severity or field representations throw.
20
+ * @param event - Event name identifying the typed notification or Gateway voice dispatch.
21
+ * @returns A diagnostic event with typed field values.
22
+ */
13
23
  export function toDiagnosticEvent(event) {
14
24
  const severity = event.severity;
15
25
  if (!isDiagnosticSeverity(severity)) {
package/native.d.ts CHANGED
@@ -102,6 +102,24 @@ export declare class NativeRemedia {
102
102
  playbackError(playbackId: string): string | null
103
103
  /** Stop and forget a standalone playback session. Voice sessions remain connected. */
104
104
  disposePlaybackSession(playbackId: string): Promise<void>
105
+ /**
106
+ * Register a cancellable probe; at most eight operations may be outstanding.
107
+ * @returns A disposable native operation identifier; rejects if closed or at capacity.
108
+ */
109
+ createProbe(): string
110
+ /**
111
+ * Cancel and release a disposable probe handle. Repeated calls are harmless.
112
+ * @param id - Operation identifier returned by createProbe.
113
+ */
114
+ cancelProbe(id: string): void
115
+ /**
116
+ * Inspect encoded input asynchronously without creating playback resources.
117
+ * @param id - Disposable operation identifier returned by createProbe.
118
+ * @param source - Encoded file, URL, owned bytes or bounded byte-stream handle.
119
+ * @param timeoutMs - Positive total deadline in milliseconds.
120
+ * @returns Available metadata; rejects invalid media, timeout, cancellation or capacity exhaustion.
121
+ */
122
+ probe(id: string, source: NativeMediaSource, timeoutMs: number): Promise<NativeStreamInfo>
105
123
  }
106
124
 
107
125
  export interface NativeDiagnosticEvent {