ink-framework-audio 0.1.0-alpha.13 → 0.1.0-alpha.14

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/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "ink-framework-audio",
3
- "version": "0.1.0-alpha.13",
3
+ "version": "0.1.0-alpha.14",
4
4
  "exports": {
5
5
  ".": "./src/index.ts",
6
6
  "./microphone": "./src/microphone.ts",
7
7
  "./recording": "./src/recording.ts"
8
8
  },
9
9
  "peerDependencies": {
10
- "@ink/files": "npm:ink-framework-files@0.1.0-alpha.13",
11
- "ink": "npm:ink-framework@0.1.0-alpha.13",
10
+ "@ink/files": "npm:ink-framework-files@0.1.0-alpha.14",
11
+ "ink": "npm:ink-framework@0.1.0-alpha.14",
12
12
  "react": "19.2.8"
13
13
  },
14
14
  "license": "MIT",
package/src/decode.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { NativeError } from "ink/native";
2
+ import type { AudioItem, PlayerState } from "./index";
3
+
4
+ function decodeItem(value: unknown): AudioItem {
5
+ if (typeof value !== "object" || value === null) throw new NativeError("protocol", "Invalid audio queue item");
6
+ const text = (name: string): string => {
7
+ const result: unknown = Reflect.get(value, name);
8
+ if (typeof result !== "string") throw new NativeError("protocol", `Invalid audio item ${name}`);
9
+ return result;
10
+ };
11
+ const duration: unknown = Reflect.get(value, "duration");
12
+ if (duration !== undefined && (typeof duration !== "number" || !Number.isFinite(duration) || duration < 0)) {
13
+ throw new NativeError("protocol", "Invalid audio item duration");
14
+ }
15
+ return { id: text("id"), src: text("src"), title: text("title"), artist: text("artist"), album: text("album"), artwork: text("artwork"), duration, usage: decodeUsage(Reflect.get(value, "usage")) };
16
+ }
17
+
18
+ function decodeUsage(value: unknown): AudioItem["usage"] {
19
+ if (value === undefined || value === "music" || value === "speech") return value;
20
+ throw new NativeError("protocol", "Invalid audio item usage");
21
+ }
22
+
23
+ export function decodePlayerState(value: unknown): PlayerState {
24
+ if (typeof value !== "object" || value === null) throw new NativeError("protocol", "Invalid player state");
25
+ const field = (name: string): unknown => Reflect.get(value, name);
26
+ const text = (name: string) => {
27
+ const result = field(name);
28
+ if (typeof result !== "string") throw new NativeError("protocol", `Invalid player ${name}`);
29
+ return result;
30
+ };
31
+ const number = (name: string) => {
32
+ const result = field(name);
33
+ if (typeof result !== "number" || !Number.isFinite(result) || result < 0) throw new NativeError("protocol", `Invalid player ${name}`);
34
+ return result;
35
+ };
36
+ const status = text("status");
37
+ if (!["idle", "loading", "playing", "paused", "ended", "error"].includes(status) || typeof field("ready") !== "boolean") {
38
+ throw new NativeError("protocol", "Invalid player status");
39
+ }
40
+ let error: Error | null = null;
41
+ if (status === "error") {
42
+ const detail = field("error");
43
+ if (typeof detail !== "object" || detail === null
44
+ || !("message" in detail) || typeof detail.message !== "string"
45
+ || !("kind" in detail) || typeof detail.kind !== "string") throw new NativeError("protocol", "Invalid player error");
46
+ error = new NativeError(detail.kind, detail.message, "retryable" in detail && detail.retryable === true);
47
+ }
48
+ const src = text("src");
49
+ const index = field("index");
50
+ if (typeof index !== "number" || !Number.isSafeInteger(index) || index < -1) {
51
+ throw new NativeError("protocol", "Invalid player queue index");
52
+ }
53
+ const duration = number("durationMs");
54
+ const queue = field("queue");
55
+ const repeat = field("repeat");
56
+ if (repeat !== "off" && repeat !== "queue" && repeat !== "track") throw new NativeError("protocol", "Invalid repeat mode");
57
+ const shuffle = field("shuffle");
58
+ if (typeof shuffle !== "boolean") throw new NativeError("protocol", "Invalid shuffle mode");
59
+ if (!Array.isArray(queue)) throw new NativeError("protocol", "Invalid audio queue");
60
+ return {
61
+ ready: field("ready") === true,
62
+ playing: status === "playing",
63
+ playWhenReady: field("playWhenReady") === true,
64
+ buffering: status === "loading",
65
+ loading: field("loading") === true,
66
+ ended: status === "ended",
67
+ queue: queue.map(decodeItem),
68
+ index,
69
+ current: src ? { id: text("id"), src, title: text("title"), artist: text("artist"), album: text("album"), artwork: text("artwork"), duration, usage: decodeUsage(field("usage")) } : null,
70
+ position: number("positionMs"), duration, speed: number("speed"),
71
+ shuffle, repeat,
72
+ skipSilence: field("skipSilence") === true, voiceBoost: field("voiceBoost") === true,
73
+ silenceSaved: number("silenceSavedMs"), error,
74
+ };
75
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "ink/internal/react";
2
2
  import { useNativeController } from "ink/internal/controller";
3
3
  import { NativeError } from "ink/native";
4
4
  import { playbackController } from "ink/internal/playback";
5
+ import { decodePlayerState } from "./decode";
5
6
 
6
7
  export interface AudioItem {
7
8
  id: string;
@@ -11,6 +12,7 @@ export interface AudioItem {
11
12
  album?: string;
12
13
  artwork?: string;
13
14
  duration?: number;
15
+ usage?: "music" | "speech";
14
16
  }
15
17
  export type SourceReplacement = { prepare?: boolean } & ({ position?: number; offset?: never } | { offset: number; position?: never });
16
18
 
@@ -20,6 +22,8 @@ export interface PlayerOptions {
20
22
  usage?: "music" | "speech";
21
23
  /** Reuse HTTP(S) audio in a shared 256 MiB disc cache. Off by default. */
22
24
  cache?: boolean;
25
+ /** Allow hardware audio offload when effects and speed changes are inactive. Off by default. */
26
+ offload?: boolean;
23
27
  /** Milliseconds to buffer for the next queued item. Off by default. */
24
28
  preloadNext?: number;
25
29
  /** Milliseconds of audio to buffer before streaming starts. Defaults to 1000. */
@@ -48,18 +52,9 @@ export interface PlayerState {
48
52
  }
49
53
  const initial: PlayerState = { ready: false, playing: false, playWhenReady: false, buffering: false, loading: false, ended: false, queue: [], current: null, index: -1, position: 0, duration: 0, speed: 1, shuffle: false, repeat: "off", skipSilence: false, voiceBoost: false, silenceSaved: 0, error: null };
50
54
 
51
- function decodeItem(value: unknown): AudioItem {
52
- if (typeof value !== "object" || value === null) throw new NativeError("protocol", "Invalid audio queue item");
53
- const text = (name: string): string => {
54
- const result: unknown = Reflect.get(value, name);
55
- if (typeof result !== "string") throw new NativeError("protocol", `Invalid audio item ${name}`);
56
- return result;
57
- };
58
- const duration: unknown = Reflect.get(value, "duration");
59
- if (duration !== undefined && (typeof duration !== "number" || !Number.isFinite(duration) || duration < 0)) {
60
- throw new NativeError("protocol", "Invalid audio item duration");
61
- }
62
- return { id: text("id"), src: text("src"), title: text("title"), artist: text("artist"), album: text("album"), artwork: text("artwork"), duration };
55
+ function validateItem(item: AudioItem) {
56
+ validateId(item.id);
57
+ if (item.usage !== undefined && item.usage !== "music" && item.usage !== "speech") throw new TypeError("Audio usage must be music or speech");
63
58
  }
64
59
 
65
60
  function validateId(id: string) {
@@ -70,69 +65,16 @@ function validateQueue(items: readonly AudioItem[]) {
70
65
  if (items.length === 0) throw new RangeError("Audio queues cannot be empty");
71
66
  const ids = new Set<string>();
72
67
  for (const item of items) {
73
- validateId(item.id);
68
+ validateItem(item);
74
69
  if (ids.has(item.id)) throw new TypeError("Audio queue item IDs must be unique");
75
70
  ids.add(item.id);
76
71
  }
77
72
  }
78
73
 
79
- function decode(value: unknown): PlayerState {
80
- if (typeof value !== "object" || value === null) throw new NativeError("protocol", "Invalid player state");
81
- const field = (name: string): unknown => Reflect.get(value, name);
82
- const text = (name: string) => {
83
- const result = field(name);
84
- if (typeof result !== "string") throw new NativeError("protocol", `Invalid player ${name}`);
85
- return result;
86
- };
87
- const number = (name: string) => {
88
- const result = field(name);
89
- if (typeof result !== "number" || !Number.isFinite(result) || result < 0) throw new NativeError("protocol", `Invalid player ${name}`);
90
- return result;
91
- };
92
- const status = text("status");
93
- if (!["idle", "loading", "playing", "paused", "ended", "error"].includes(status) || typeof field("ready") !== "boolean") {
94
- throw new NativeError("protocol", "Invalid player status");
95
- }
96
- let error: Error | null = null;
97
- if (status === "error") {
98
- const detail = field("error");
99
- if (typeof detail !== "object" || detail === null
100
- || !("message" in detail) || typeof detail.message !== "string"
101
- || !("kind" in detail) || typeof detail.kind !== "string") throw new NativeError("protocol", "Invalid player error");
102
- error = new NativeError(detail.kind, detail.message, "retryable" in detail && detail.retryable === true);
103
- }
104
- const src = text("src");
105
- const index = field("index");
106
- if (typeof index !== "number" || !Number.isSafeInteger(index) || index < -1) {
107
- throw new NativeError("protocol", "Invalid player queue index");
108
- }
109
- const duration = number("durationMs");
110
- const queue = field("queue");
111
- const repeat = field("repeat");
112
- if (repeat !== "off" && repeat !== "queue" && repeat !== "track") throw new NativeError("protocol", "Invalid repeat mode");
113
- const shuffle = field("shuffle");
114
- if (typeof shuffle !== "boolean") throw new NativeError("protocol", "Invalid shuffle mode");
115
- if (!Array.isArray(queue)) throw new NativeError("protocol", "Invalid audio queue");
116
- return {
117
- ready: field("ready") === true,
118
- playing: status === "playing",
119
- playWhenReady: field("playWhenReady") === true,
120
- buffering: status === "loading",
121
- loading: field("loading") === true,
122
- ended: status === "ended",
123
- queue: queue.map(decodeItem),
124
- index,
125
- current: src ? { id: text("id"), src, title: text("title"), artist: text("artist"), album: text("album"), artwork: text("artwork"), duration } : null,
126
- position: number("positionMs"), duration, speed: number("speed"),
127
- shuffle, repeat,
128
- skipSilence: field("skipSilence") === true, voiceBoost: field("voiceBoost") === true,
129
- silenceSaved: number("silenceSavedMs"), error,
130
- };
131
- }
132
-
133
74
  export function usePlayer(options: PlayerOptions = {}) {
134
- const { session = "main", mode = "attached", usage = "music", cache = false, preloadNext = 0, startupBuffer = 1000 } = options;
75
+ const { session = "main", mode = "attached", usage = "music", cache = false, offload = false, preloadNext = 0, startupBuffer = 1000 } = options;
135
76
  if (!/^[A-Za-z0-9._-]{1,64}$/.test(session)) throw new TypeError("Audio session names must be 1–64 letters, numbers, dots, underscores or hyphens");
77
+ if (usage !== "music" && usage !== "speech") throw new TypeError("Audio usage must be music or speech");
136
78
  if (!Number.isSafeInteger(preloadNext) || preloadNext < 0 || preloadNext > 30_000) throw new RangeError("Next-item preloading must be between 0 and 30000 milliseconds");
137
79
  if (!Number.isSafeInteger(startupBuffer) || startupBuffer < 0 || startupBuffer > 30_000) throw new RangeError("Startup buffering must be between 0 and 30000 milliseconds");
138
80
  const [state, setState] = useState(initial);
@@ -141,8 +83,8 @@ export function usePlayer(options: PlayerOptions = {}) {
141
83
  useEffect(() => {
142
84
  const publish = (next: PlayerState) => { current.current = next; setState(next); };
143
85
  publish(initial);
144
- const detach = controller.attach({ kind: "player", config: { session, playback: mode, usage, cache, preloadNext, startupBuffer } }, value => {
145
- try { publish(decode(value)); }
86
+ const detach = controller.attach({ kind: "player", config: { session, playback: mode, usage, cache, offload, preloadNext, startupBuffer } }, value => {
87
+ try { publish(decodePlayerState(value)); }
146
88
  catch (error) { publish({ ...initial, error: error instanceof Error ? error : new Error(String(error)) }); }
147
89
  }, error => {
148
90
  publish({ ...initial, error: error instanceof Error ? error : new Error(String(error)) });
@@ -151,7 +93,7 @@ export function usePlayer(options: PlayerOptions = {}) {
151
93
  detach();
152
94
  current.current = initial;
153
95
  };
154
- }, [session, mode, usage, cache, preloadNext, startupBuffer, controller]);
96
+ }, [session, mode, usage, cache, offload, preloadNext, startupBuffer, controller]);
155
97
  const commands = useMemo(() => {
156
98
  const call = async (operation: string, payload: unknown = {}) => {
157
99
  if (!controller.current || !current.current.ready) throw new NativeError("unavailable", "Audio player is not ready");
@@ -161,7 +103,7 @@ export function usePlayer(options: PlayerOptions = {}) {
161
103
  async getState(): Promise<PlayerState> {
162
104
  const active = controller.current;
163
105
  if (!active || !current.current.ready) throw new NativeError("unavailable", "Audio player is not ready");
164
- return decode(JSON.parse(await active.call("getState")));
106
+ return decodePlayerState(JSON.parse(await active.call("getState")));
165
107
  },
166
108
  seekBy(offset: number) {
167
109
  if (!Number.isSafeInteger(offset)) return Promise.reject(new RangeError("Seek offset must be a safe integer in milliseconds"));
@@ -183,7 +125,7 @@ export function usePlayer(options: PlayerOptions = {}) {
183
125
  await call("setQueue", { items, startIndex: options.startIndex ?? 0, startPosition, prepare: options.prepare ?? true });
184
126
  },
185
127
  async add(item: AudioItem, options: { at?: number | "next" } = {}) {
186
- validateId(item.id);
128
+ validateItem(item);
187
129
  if (options.at !== undefined && options.at !== "next" && (!Number.isSafeInteger(options.at) || options.at < 0)) {
188
130
  throw new RangeError("Audio insertion index must be a non-negative integer");
189
131
  }
@@ -195,7 +137,7 @@ export function usePlayer(options: PlayerOptions = {}) {
195
137
  await call("move", { id, index });
196
138
  },
197
139
  async replace(id: string, item: AudioItem) {
198
- validateId(item.id);
140
+ validateItem(item);
199
141
  await call("replace", { id, item });
200
142
  },
201
143
  async play(id?: string) {