@gentomiyano/optimized-web-audio-player 0.1.0

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.
Files changed (53) hide show
  1. package/README.md +159 -0
  2. package/dist/config/index.d.ts +37 -0
  3. package/dist/config/index.js +280 -0
  4. package/dist/config/index.js.map +1 -0
  5. package/dist/consumer/index.d.ts +27 -0
  6. package/dist/consumer/index.js +38 -0
  7. package/dist/consumer/index.js.map +1 -0
  8. package/dist/consumer-smoke/node_modules/.modules.yaml +30 -0
  9. package/dist/consumer-smoke/node_modules/.package-map.json +1 -0
  10. package/dist/consumer-smoke/node_modules/.pnpm/lock.yaml +57 -0
  11. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/LICENSE +21 -0
  12. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/README.md +9 -0
  13. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_mock.development.js +414 -0
  14. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_mock.production.js +406 -0
  15. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_post_task.development.js +150 -0
  16. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler-unstable_post_task.production.js +140 -0
  17. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.development.js +364 -0
  18. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.native.development.js +350 -0
  19. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.native.production.js +330 -0
  20. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/cjs/scheduler.production.js +340 -0
  21. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/index.js +7 -0
  22. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/index.native.js +7 -0
  23. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/package.json +27 -0
  24. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/unstable_mock.js +7 -0
  25. package/dist/consumer-smoke/node_modules/.pnpm/scheduler@0.27.0/node_modules/scheduler/unstable_post_task.js +7 -0
  26. package/dist/consumer-smoke/node_modules/.pnpm-workspace-state-v1.json +29 -0
  27. package/dist/contracts/index.d.ts +15 -0
  28. package/dist/contracts/index.js +3 -0
  29. package/dist/contracts/index.js.map +1 -0
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.js +368 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/media-delivery/index.d.ts +185 -0
  34. package/dist/media-delivery/index.js +91 -0
  35. package/dist/media-delivery/index.js.map +1 -0
  36. package/dist/media-element-C9dis4aG.d.ts +40 -0
  37. package/dist/player-DE9Vqztu.d.ts +212 -0
  38. package/dist/ports/index.d.ts +32 -0
  39. package/dist/ports/index.js +3 -0
  40. package/dist/ports/index.js.map +1 -0
  41. package/dist/react/index.d.ts +86 -0
  42. package/dist/react/index.js +2393 -0
  43. package/dist/react/index.js.map +1 -0
  44. package/dist/testing/index.d.ts +36 -0
  45. package/dist/testing/index.js +365 -0
  46. package/dist/testing/index.js.map +1 -0
  47. package/dist/ui/index.d.ts +143 -0
  48. package/dist/ui/index.js +1344 -0
  49. package/dist/ui/index.js.map +1 -0
  50. package/dist/ui/player-tokens.css +572 -0
  51. package/dist/ui/player-views.css +654 -0
  52. package/dist/ui/styles.css +2 -0
  53. package/package.json +97 -0
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # Optimized Web Audio Player
2
+
3
+ Consumer-neutral Music Player package published as
4
+ `@gentomiyano/optimized-web-audio-player`.
5
+
6
+ ## MVP status
7
+
8
+ The standalone Player MVP was accepted on 2026-08-01. The accepted scope
9
+ includes the framework-independent Core, reusable React Runtime and UI,
10
+ Compact / Wide / Detailed / Podcast / Mini views, Queue, Debug customization,
11
+ local-media Showcase tooling, Media Session integration, responsive mobile
12
+ review, and the media-delivery boundary contract.
13
+
14
+ The tested iOS device continued playback in the background, during sleep, and
15
+ on the Lock Screen. Lock Screen Previous / Next and Seek worked, and starting
16
+ audio in another application interrupted the Player as expected. iOS uses the
17
+ continuity-first native HTML media path by default; fade timing is not
18
+ perceptible there and is an accepted MVP platform limitation. Narrow Compact
19
+ and Mini layouts, including Global Mini, omit their in-player volume control
20
+ because iOS owns device output volume.
21
+
22
+ This acceptance does not connect a consumer application or production media
23
+ origin, and it is not a claim of identical behavior across every iOS version,
24
+ browser, Bluetooth route, or device.
25
+
26
+ ## WP-07 consumer integration foundation
27
+
28
+ The production React Runtime, UI, CSS, and consumer queue builder now have
29
+ explicit package entry points. A consumer can mount one empty
30
+ `PlayerProvider` in its application shell, map authorized public media through
31
+ its own Host Adapter, and build collection, selection, or page-order queues
32
+ without copying Player source.
33
+
34
+ Official Website and Timeline are not changed in this phase. Protected-URL
35
+ refresh and the first consumer connection remain explicit checkpoints. See
36
+ `docs/WP-07-CONSUMER-INTEGRATION-FOUNDATION.md`.
37
+
38
+ The public npm registry, scoped package name, and release workflow are fixed.
39
+ Initial npm publication still requires the scope owner's authentication.
40
+ Release and consumer pinning rules are documented in `docs/RELEASING.md`.
41
+
42
+ ## WP-06 debug local media import
43
+
44
+ The standalone Showcase has a dev-only Local Media panel for dropping WAV,
45
+ MP3, JPG/JPEG, PNG, and WebP files. Multi-file audio drops become a temporary
46
+ Queue in drop order. Metadata overrides, artwork switching, track-number
47
+ sorting, duplicate policy, re-read, reorder, delete, clear, and cancellable
48
+ waveform analysis remain in tab memory only.
49
+
50
+ No local file, metadata, filename, or object URL is sent, logged, persisted, or
51
+ written back. Reloading clears the import. Audio and artwork object URLs are
52
+ reference-counted, reused for duplicate identities, and revoked after the last
53
+ item is removed, on clear, or when the Showcase unmounts.
54
+
55
+ The parser and UI live only under `showcase/local-media/**`. They are not
56
+ package exports or `tsup` entries. `pnpm build` scans every production
57
+ JavaScript output for WP-06 markers and fails if the debug importer leaks into
58
+ the package. See `docs/WP-06-DEBUG-MEDIA-IMPORT.md` for format coverage,
59
+ limitations, and the manual checkpoint.
60
+
61
+ ## WP-05 background playback checkpoint
62
+
63
+ The standalone Showcase now integrates Media Session metadata, playback and
64
+ position state, and OS action handlers. Music exposes play, pause, stop,
65
+ Previous, Next, and seek-to. Podcast additionally exposes configurable skip
66
+ backward and forward actions. Every handler delegates to the existing
67
+ `PlayerCommands`; initial queue load remains non-autoplay.
68
+
69
+ `audioMode=auto` keeps Web Audio fades on non-iOS browsers and uses the native
70
+ HTMLAudioElement path on iPhone/iPadOS, prioritizing background continuity over
71
+ fades. Real-device comparison modes are:
72
+
73
+ - `http://127.0.0.1:4274/?audioMode=web-audio`
74
+ - `http://127.0.0.1:4274/?audioMode=html-media`
75
+
76
+ Double-click `start-player-showcase-device.cmd` for same-LAN iPhone testing,
77
+ then replace the host with this computer's private LAN IP. The original
78
+ `start-player-showcase.cmd` remains the loopback-only desktop launcher.
79
+ `start-player-showcase-mobile.cmd` is an equivalent mobile-review launcher.
80
+ The Showcase also has a `Mobile preview` toggle; `?preview=mobile` opens the
81
+ same constrained review shell directly on desktop.
82
+
83
+ Automated verification and the accepted device observations are recorded in
84
+ `docs/WP-05A-BACKGROUND-PLAYBACK-FOUNDATION.md`. Broader device/version,
85
+ Bluetooth, and production-consumer claims still require their own acceptance
86
+ record.
87
+
88
+ ## WP-03 runtime showcase
89
+
90
+ Double-click `start-player-showcase.cmd` to open the standalone Showcase at
91
+ `http://127.0.0.1:4274/`. The runtime keeps one Core store, one browser audio
92
+ element, and one lazily-created Web Audio gain graph above the Compact, Wide,
93
+ Detailed, Podcast, and Mini view switcher. Queue Panel and the fixed Global
94
+ Mini also consume that same state. Switching views therefore does not stop
95
+ playback or create another audio element.
96
+
97
+ The Showcase uses generated music and podcast WAV tone fixtures. They are local,
98
+ copyright-free integration media only and are not production assets. The
99
+ runtime connects UI exclusively through `PlayerState` and `PlayerCommands`;
100
+ the waveform remains visual-only and seeking is owned by the Seek Bar.
101
+
102
+ Manual UX check:
103
+
104
+ 1. Start playback and confirm that Audio Context changes from `uninitialized`
105
+ to `running` only after the first user action.
106
+ 2. Check Play/Pause, Seek, Volume, Previous, and Next in Wide view. On desktop,
107
+ open the compact vertical Volume control from its icon in Compact and Mini;
108
+ narrow mobile layouts intentionally omit it.
109
+ 3. While playing, switch between Compact, Wide, Detailed, Podcast, and Mini and
110
+ confirm that the same track, time, and playback state continue.
111
+ 4. Open Queue Panel, select another item, and check the Podcast queue with
112
+ skip-back/forward controls.
113
+ 5. Toggle Global Mini and confirm that the page bottom inset is reserved only
114
+ while it is visible.
115
+ 6. In browser developer tools, confirm that
116
+ `document.querySelectorAll('audio[data-player-audio="true"]').length` is
117
+ always `1`.
118
+
119
+ ## WP-01 entry points
120
+
121
+ - `@gentomiyano/optimized-web-audio-player/contracts`: immutable state, commands, lifecycle,
122
+ host adapter, config, preset, and debug-controller contracts
123
+ - `@gentomiyano/optimized-web-audio-player/ports`: small media, gain, and persistence ports
124
+ - `@gentomiyano/optimized-web-audio-player/config`: preview defaults, strict validation,
125
+ versioned Config JSON schema v2, and initial state
126
+ - `@gentomiyano/optimized-web-audio-player/media-delivery`: WP-04 URL-free delivery
127
+ descriptors, ephemeral playback-lease and Host Adapter contracts, upload
128
+ evidence validation, and waveform validation
129
+ - `@gentomiyano/optimized-web-audio-player/consumer`: ordered consumer-record to Core Queue
130
+ conversion for already-authorized directly playable media
131
+ - `@gentomiyano/optimized-web-audio-player/react`: the single-audio React Runtime and hook
132
+ - `@gentomiyano/optimized-web-audio-player/ui`: reusable Player views and primitives
133
+ - `@gentomiyano/optimized-web-audio-player/styles.css`: production UI tokens and styles
134
+ - `@gentomiyano/optimized-web-audio-player/testing`: privacy-safe fixtures and mock scenarios
135
+
136
+ The root entry point exports production contracts, ports, config, and the
137
+ provider-neutral media-delivery contract only. Runtime/UI are opt-in subpaths,
138
+ and testing helpers are isolated behind `/testing`. No debug Runtime, Showcase,
139
+ local importer, consumer-specific adapter, or production media URL is included
140
+ in the package root.
141
+
142
+ ## Commands
143
+
144
+ ```text
145
+ pnpm lint
146
+ pnpm test
147
+ pnpm typecheck
148
+ pnpm build
149
+ pnpm showcase:build
150
+ ```
151
+
152
+ ## Parallel ownership after the WP-01 checkpoint
153
+
154
+ - Core: `src/core/**`, `tests/core/**`
155
+ - WP-02B UI ownership explicitly includes `src/ui/**`, `src/icons/**`,
156
+ `src/debug/ui/**`, and `tests/ui/**`.
157
+ - UI may also render its mock views inside the pre-created `showcase/**` shell.
158
+ - Integration-owned files, including contracts, ports, package configuration,
159
+ adapters, and testing fixtures, must not be changed by Core or UI work.
@@ -0,0 +1,37 @@
1
+ import { l as PlayerExperienceConfig, u as PlayerState } from '../player-DE9Vqztu.js';
2
+ import { PlayerExperienceConfigValidationResult } from '../contracts/index.js';
3
+
4
+ declare const PLAYER_EXPERIENCE_CONFIG_SCHEMA_VERSION: 2;
5
+ declare const PLAYER_EXPERIENCE_CONFIG_LIMITS: Readonly<{
6
+ fadeMs: Readonly<{
7
+ min: 0;
8
+ max: 10000;
9
+ }>;
10
+ previousRestartThresholdSec: Readonly<{
11
+ min: 0;
12
+ max: 60;
13
+ }>;
14
+ waveformOpacity: Readonly<{
15
+ min: 0;
16
+ max: 1;
17
+ }>;
18
+ initialVolume: Readonly<{
19
+ min: 0;
20
+ max: 1;
21
+ }>;
22
+ podcastSkipSec: Readonly<{
23
+ minExclusive: 0;
24
+ max: 3600;
25
+ }>;
26
+ }>;
27
+ declare const PREVIEW_PLAYER_EXPERIENCE_CONFIG: Readonly<PlayerExperienceConfig>;
28
+
29
+ declare function createInitialPlayerState(experienceConfig?: Partial<PlayerExperienceConfig>): Readonly<PlayerState>;
30
+
31
+ declare function validatePlayerExperienceConfig(value: unknown): PlayerExperienceConfigValidationResult;
32
+ declare function validatePlayerExperienceConfigOverride(value: unknown): PlayerExperienceConfigValidationResult;
33
+ declare function resolvePlayerExperienceConfig(override?: Partial<PlayerExperienceConfig>): PlayerExperienceConfigValidationResult;
34
+ declare function parsePlayerExperienceConfigJson(json: string): PlayerExperienceConfigValidationResult;
35
+ declare function serializePlayerExperienceConfig(config: PlayerExperienceConfig): string;
36
+
37
+ export { PLAYER_EXPERIENCE_CONFIG_LIMITS, PLAYER_EXPERIENCE_CONFIG_SCHEMA_VERSION, PREVIEW_PLAYER_EXPERIENCE_CONFIG, createInitialPlayerState, parsePlayerExperienceConfigJson, resolvePlayerExperienceConfig, serializePlayerExperienceConfig, validatePlayerExperienceConfig, validatePlayerExperienceConfigOverride };
@@ -0,0 +1,280 @@
1
+ // src/config/defaults.ts
2
+ var PLAYER_EXPERIENCE_CONFIG_SCHEMA_VERSION = 2;
3
+ var PLAYER_EXPERIENCE_CONFIG_LIMITS = Object.freeze({
4
+ fadeMs: Object.freeze({ min: 0, max: 1e4 }),
5
+ previousRestartThresholdSec: Object.freeze({ min: 0, max: 60 }),
6
+ waveformOpacity: Object.freeze({ min: 0, max: 1 }),
7
+ initialVolume: Object.freeze({ min: 0, max: 1 }),
8
+ podcastSkipSec: Object.freeze({ minExclusive: 0, max: 3600 })
9
+ });
10
+ var PREVIEW_PLAYER_EXPERIENCE_CONFIG = Object.freeze({
11
+ playFadeInMs: 300,
12
+ pauseFadeOutMs: 600,
13
+ trackChangeFadeOutMs: 300,
14
+ trackChangeFadeInMs: 300,
15
+ seekFadeMs: 120,
16
+ fadeCurve: "ease-out",
17
+ previousRestartThresholdSec: 3,
18
+ seekCommitMode: "on-release",
19
+ waveformPlayedStateEnabled: true,
20
+ waveformOpacity: 0.72,
21
+ initialShuffleEnabled: false,
22
+ initialRepeatMode: "off",
23
+ initialVolume: 0.8,
24
+ persistMode: "none",
25
+ podcastBackSec: 15,
26
+ podcastForwardSec: 30,
27
+ podcastPreviousMode: "previous-episode"
28
+ });
29
+
30
+ // src/config/validation.ts
31
+ var CONFIG_KEYS = [
32
+ "playFadeInMs",
33
+ "pauseFadeOutMs",
34
+ "trackChangeFadeOutMs",
35
+ "trackChangeFadeInMs",
36
+ "seekFadeMs",
37
+ "fadeCurve",
38
+ "previousRestartThresholdSec",
39
+ "seekCommitMode",
40
+ "waveformPlayedStateEnabled",
41
+ "waveformOpacity",
42
+ "initialShuffleEnabled",
43
+ "initialRepeatMode",
44
+ "initialVolume",
45
+ "persistMode",
46
+ "podcastBackSec",
47
+ "podcastForwardSec",
48
+ "podcastPreviousMode"
49
+ ];
50
+ var FADE_CURVES = /* @__PURE__ */ new Set([
51
+ "linear",
52
+ "ease-in",
53
+ "ease-out",
54
+ "equal-power"
55
+ ]);
56
+ var SEEK_COMMIT_MODES = /* @__PURE__ */ new Set(["live", "on-release"]);
57
+ var REPEAT_MODES = /* @__PURE__ */ new Set(["off", "all", "one"]);
58
+ var PERSIST_MODES = /* @__PURE__ */ new Set(["none", "session", "local"]);
59
+ var PODCAST_PREVIOUS_MODES = /* @__PURE__ */ new Set([
60
+ "previous-episode",
61
+ "restart-episode"
62
+ ]);
63
+ function isRecord(value) {
64
+ return typeof value === "object" && value !== null && !Array.isArray(value);
65
+ }
66
+ function validateFiniteRange(value, path, min, max, errors) {
67
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
68
+ errors.push(
69
+ `${path} must be a finite number between ${String(min)} and ${String(max)}.`
70
+ );
71
+ return false;
72
+ }
73
+ return true;
74
+ }
75
+ function validateBoolean(value, path, errors) {
76
+ if (typeof value !== "boolean") {
77
+ errors.push(`${path} must be a boolean.`);
78
+ return false;
79
+ }
80
+ return true;
81
+ }
82
+ function validateConfigRecord(value, options) {
83
+ if (!isRecord(value)) {
84
+ return { ok: false, errors: ["config must be an object."] };
85
+ }
86
+ const errors = [];
87
+ const allowedKeys = new Set(CONFIG_KEYS);
88
+ for (const key of Object.keys(value)) {
89
+ if (!allowedKeys.has(key)) {
90
+ errors.push(`config.${key} is not supported.`);
91
+ }
92
+ }
93
+ if (!options.partial) {
94
+ for (const key of CONFIG_KEYS) {
95
+ if (!Object.hasOwn(value, key)) {
96
+ errors.push(`config.${key} is required.`);
97
+ }
98
+ }
99
+ }
100
+ const has = (key) => Object.hasOwn(value, key);
101
+ const fadeLimits = PLAYER_EXPERIENCE_CONFIG_LIMITS.fadeMs;
102
+ for (const key of [
103
+ "playFadeInMs",
104
+ "pauseFadeOutMs",
105
+ "trackChangeFadeOutMs",
106
+ "trackChangeFadeInMs",
107
+ "seekFadeMs"
108
+ ]) {
109
+ if (has(key)) {
110
+ validateFiniteRange(value[key], `config.${key}`, fadeLimits.min, fadeLimits.max, errors);
111
+ }
112
+ }
113
+ const thresholdLimits = PLAYER_EXPERIENCE_CONFIG_LIMITS.previousRestartThresholdSec;
114
+ if (has("previousRestartThresholdSec")) {
115
+ validateFiniteRange(
116
+ value.previousRestartThresholdSec,
117
+ "config.previousRestartThresholdSec",
118
+ thresholdLimits.min,
119
+ thresholdLimits.max,
120
+ errors
121
+ );
122
+ }
123
+ const volumeLimits = PLAYER_EXPERIENCE_CONFIG_LIMITS.initialVolume;
124
+ if (has("initialVolume")) {
125
+ validateFiniteRange(
126
+ value.initialVolume,
127
+ "config.initialVolume",
128
+ volumeLimits.min,
129
+ volumeLimits.max,
130
+ errors
131
+ );
132
+ }
133
+ const waveformOpacityLimits = PLAYER_EXPERIENCE_CONFIG_LIMITS.waveformOpacity;
134
+ if (has("waveformOpacity")) {
135
+ validateFiniteRange(
136
+ value.waveformOpacity,
137
+ "config.waveformOpacity",
138
+ waveformOpacityLimits.min,
139
+ waveformOpacityLimits.max,
140
+ errors
141
+ );
142
+ }
143
+ for (const key of [
144
+ "waveformPlayedStateEnabled",
145
+ "initialShuffleEnabled"
146
+ ]) {
147
+ if (has(key)) {
148
+ validateBoolean(value[key], `config.${key}`, errors);
149
+ }
150
+ }
151
+ const skipLimits = PLAYER_EXPERIENCE_CONFIG_LIMITS.podcastSkipSec;
152
+ for (const key of ["podcastBackSec", "podcastForwardSec"]) {
153
+ if (has(key)) {
154
+ const candidate = value[key];
155
+ const valid = validateFiniteRange(
156
+ candidate,
157
+ `config.${key}`,
158
+ skipLimits.minExclusive,
159
+ skipLimits.max,
160
+ errors
161
+ );
162
+ if (valid && candidate <= skipLimits.minExclusive) {
163
+ errors.push(`config.${key} must be greater than 0.`);
164
+ }
165
+ }
166
+ }
167
+ if (has("fadeCurve") && !FADE_CURVES.has(value.fadeCurve)) {
168
+ errors.push("config.fadeCurve is not supported.");
169
+ }
170
+ if (has("seekCommitMode") && !SEEK_COMMIT_MODES.has(value.seekCommitMode)) {
171
+ errors.push("config.seekCommitMode is not supported.");
172
+ }
173
+ if (has("initialRepeatMode") && !REPEAT_MODES.has(value.initialRepeatMode)) {
174
+ errors.push("config.initialRepeatMode is not supported.");
175
+ }
176
+ if (has("persistMode") && !PERSIST_MODES.has(value.persistMode)) {
177
+ errors.push("config.persistMode is not supported.");
178
+ }
179
+ if (has("podcastPreviousMode") && !PODCAST_PREVIOUS_MODES.has(
180
+ value.podcastPreviousMode
181
+ )) {
182
+ errors.push("config.podcastPreviousMode is not supported.");
183
+ }
184
+ if (errors.length > 0) {
185
+ return { ok: false, errors: Object.freeze(errors) };
186
+ }
187
+ const merged = options.partial ? { ...PREVIEW_PLAYER_EXPERIENCE_CONFIG, ...value } : value;
188
+ return {
189
+ ok: true,
190
+ errors: [],
191
+ value: Object.freeze({ ...merged })
192
+ };
193
+ }
194
+ function validatePlayerExperienceConfig(value) {
195
+ return validateConfigRecord(value, { partial: false });
196
+ }
197
+ function validatePlayerExperienceConfigOverride(value) {
198
+ return validateConfigRecord(value, { partial: true });
199
+ }
200
+ function resolvePlayerExperienceConfig(override) {
201
+ return validatePlayerExperienceConfigOverride(override ?? {});
202
+ }
203
+ function parsePlayerExperienceConfigJson(json) {
204
+ let parsed;
205
+ try {
206
+ parsed = JSON.parse(json);
207
+ } catch {
208
+ return { ok: false, errors: ["Config JSON is not valid JSON."] };
209
+ }
210
+ if (!isRecord(parsed)) {
211
+ return { ok: false, errors: ["Config JSON envelope must be an object."] };
212
+ }
213
+ const envelopeKeys = Object.keys(parsed);
214
+ const errors = [];
215
+ for (const key of envelopeKeys) {
216
+ if (key !== "schemaVersion" && key !== "config") {
217
+ errors.push(`envelope.${key} is not supported.`);
218
+ }
219
+ }
220
+ if (!Object.hasOwn(parsed, "schemaVersion")) {
221
+ errors.push("envelope.schemaVersion is required.");
222
+ } else if (parsed.schemaVersion !== PLAYER_EXPERIENCE_CONFIG_SCHEMA_VERSION) {
223
+ errors.push("envelope.schemaVersion is not supported.");
224
+ }
225
+ if (!Object.hasOwn(parsed, "config")) {
226
+ errors.push("envelope.config is required.");
227
+ }
228
+ if (errors.length > 0) {
229
+ return { ok: false, errors: Object.freeze(errors) };
230
+ }
231
+ return validatePlayerExperienceConfig(parsed.config);
232
+ }
233
+ function serializePlayerExperienceConfig(config) {
234
+ const result = validatePlayerExperienceConfig(config);
235
+ if (!result.ok) {
236
+ throw new TypeError(result.errors.join(" "));
237
+ }
238
+ return JSON.stringify(
239
+ {
240
+ schemaVersion: PLAYER_EXPERIENCE_CONFIG_SCHEMA_VERSION,
241
+ config: result.value
242
+ },
243
+ null,
244
+ 2
245
+ );
246
+ }
247
+
248
+ // src/config/initial-state.ts
249
+ function createInitialPlayerState(experienceConfig) {
250
+ const resolved = resolvePlayerExperienceConfig(experienceConfig);
251
+ if (!resolved.ok) {
252
+ throw new TypeError(resolved.errors.join(" "));
253
+ }
254
+ return Object.freeze({
255
+ currentItem: null,
256
+ currentItemId: null,
257
+ currentQueueItemId: null,
258
+ mediaKind: null,
259
+ status: "idle",
260
+ currentTimeSec: 0,
261
+ durationSec: null,
262
+ volume: resolved.value.initialVolume,
263
+ muted: false,
264
+ shuffleEnabled: resolved.value.initialShuffleEnabled,
265
+ repeatMode: resolved.value.initialRepeatMode,
266
+ queue: Object.freeze([]),
267
+ currentQueueIndex: null,
268
+ canSeek: false,
269
+ canGoPrevious: false,
270
+ canGoNext: false,
271
+ needsUserGesture: false,
272
+ lastTransitionCause: null,
273
+ error: null,
274
+ experienceConfig: resolved.value
275
+ });
276
+ }
277
+
278
+ export { PLAYER_EXPERIENCE_CONFIG_LIMITS, PLAYER_EXPERIENCE_CONFIG_SCHEMA_VERSION, PREVIEW_PLAYER_EXPERIENCE_CONFIG, createInitialPlayerState, parsePlayerExperienceConfigJson, resolvePlayerExperienceConfig, serializePlayerExperienceConfig, validatePlayerExperienceConfig, validatePlayerExperienceConfigOverride };
279
+ //# sourceMappingURL=index.js.map
280
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/config/defaults.ts","../../src/config/validation.ts","../../src/config/initial-state.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { PlayerExperienceConfig } from \"../contracts/player\";\n\nexport const PLAYER_EXPERIENCE_CONFIG_SCHEMA_VERSION = 2 as const;\n\nexport const PLAYER_EXPERIENCE_CONFIG_LIMITS = Object.freeze({\n fadeMs: Object.freeze({ min: 0, max: 10_000 }),\n previousRestartThresholdSec: Object.freeze({ min: 0, max: 60 }),\n waveformOpacity: Object.freeze({ min: 0, max: 1 }),\n initialVolume: Object.freeze({ min: 0, max: 1 }),\n podcastSkipSec: Object.freeze({ minExclusive: 0, max: 3_600 }),\n});\n\nexport const PREVIEW_PLAYER_EXPERIENCE_CONFIG: Readonly<PlayerExperienceConfig> =\n Object.freeze({\n playFadeInMs: 300,\n pauseFadeOutMs: 600,\n trackChangeFadeOutMs: 300,\n trackChangeFadeInMs: 300,\n seekFadeMs: 120,\n fadeCurve: \"ease-out\",\n previousRestartThresholdSec: 3,\n seekCommitMode: \"on-release\",\n waveformPlayedStateEnabled: true,\n waveformOpacity: 0.72,\n initialShuffleEnabled: false,\n initialRepeatMode: \"off\",\n initialVolume: 0.8,\n persistMode: \"none\",\n podcastBackSec: 15,\n podcastForwardSec: 30,\n podcastPreviousMode: \"previous-episode\",\n });\n","import type {\n FadeCurve,\n PersistMode,\n PlayerExperienceConfig,\n PodcastPreviousMode,\n RepeatMode,\n SeekCommitMode,\n} from \"../contracts/player\";\nimport type { PlayerExperienceConfigValidationResult } from \"../contracts/config\";\nimport {\n PLAYER_EXPERIENCE_CONFIG_LIMITS,\n PLAYER_EXPERIENCE_CONFIG_SCHEMA_VERSION,\n PREVIEW_PLAYER_EXPERIENCE_CONFIG,\n} from \"./defaults\";\n\nconst CONFIG_KEYS = [\n \"playFadeInMs\",\n \"pauseFadeOutMs\",\n \"trackChangeFadeOutMs\",\n \"trackChangeFadeInMs\",\n \"seekFadeMs\",\n \"fadeCurve\",\n \"previousRestartThresholdSec\",\n \"seekCommitMode\",\n \"waveformPlayedStateEnabled\",\n \"waveformOpacity\",\n \"initialShuffleEnabled\",\n \"initialRepeatMode\",\n \"initialVolume\",\n \"persistMode\",\n \"podcastBackSec\",\n \"podcastForwardSec\",\n \"podcastPreviousMode\",\n] as const satisfies readonly (keyof PlayerExperienceConfig)[];\n\nconst FADE_CURVES = new Set<FadeCurve>([\n \"linear\",\n \"ease-in\",\n \"ease-out\",\n \"equal-power\",\n]);\nconst SEEK_COMMIT_MODES = new Set<SeekCommitMode>([\"live\", \"on-release\"]);\nconst REPEAT_MODES = new Set<RepeatMode>([\"off\", \"all\", \"one\"]);\nconst PERSIST_MODES = new Set<PersistMode>([\"none\", \"session\", \"local\"]);\nconst PODCAST_PREVIOUS_MODES = new Set<PodcastPreviousMode>([\n \"previous-episode\",\n \"restart-episode\",\n]);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction validateFiniteRange(\n value: unknown,\n path: string,\n min: number,\n max: number,\n errors: string[],\n): value is number {\n if (\n typeof value !== \"number\" ||\n !Number.isFinite(value) ||\n value < min ||\n value > max\n ) {\n errors.push(\n `${path} must be a finite number between ${String(min)} and ${String(max)}.`,\n );\n return false;\n }\n return true;\n}\n\nfunction validateBoolean(\n value: unknown,\n path: string,\n errors: string[],\n): value is boolean {\n if (typeof value !== \"boolean\") {\n errors.push(`${path} must be a boolean.`);\n return false;\n }\n return true;\n}\n\nfunction validateConfigRecord(\n value: unknown,\n options: { readonly partial: boolean },\n): PlayerExperienceConfigValidationResult {\n if (!isRecord(value)) {\n return { ok: false, errors: [\"config must be an object.\"] };\n }\n\n const errors: string[] = [];\n const allowedKeys = new Set<string>(CONFIG_KEYS);\n\n for (const key of Object.keys(value)) {\n if (!allowedKeys.has(key)) {\n errors.push(`config.${key} is not supported.`);\n }\n }\n\n if (!options.partial) {\n for (const key of CONFIG_KEYS) {\n if (!Object.hasOwn(value, key)) {\n errors.push(`config.${key} is required.`);\n }\n }\n }\n\n const has = (key: keyof PlayerExperienceConfig): boolean =>\n Object.hasOwn(value, key);\n const fadeLimits = PLAYER_EXPERIENCE_CONFIG_LIMITS.fadeMs;\n\n for (const key of [\n \"playFadeInMs\",\n \"pauseFadeOutMs\",\n \"trackChangeFadeOutMs\",\n \"trackChangeFadeInMs\",\n \"seekFadeMs\",\n ] as const) {\n if (has(key)) {\n validateFiniteRange(value[key], `config.${key}`, fadeLimits.min, fadeLimits.max, errors);\n }\n }\n\n const thresholdLimits =\n PLAYER_EXPERIENCE_CONFIG_LIMITS.previousRestartThresholdSec;\n if (has(\"previousRestartThresholdSec\")) {\n validateFiniteRange(\n value.previousRestartThresholdSec,\n \"config.previousRestartThresholdSec\",\n thresholdLimits.min,\n thresholdLimits.max,\n errors,\n );\n }\n\n const volumeLimits = PLAYER_EXPERIENCE_CONFIG_LIMITS.initialVolume;\n if (has(\"initialVolume\")) {\n validateFiniteRange(\n value.initialVolume,\n \"config.initialVolume\",\n volumeLimits.min,\n volumeLimits.max,\n errors,\n );\n }\n\n const waveformOpacityLimits =\n PLAYER_EXPERIENCE_CONFIG_LIMITS.waveformOpacity;\n if (has(\"waveformOpacity\")) {\n validateFiniteRange(\n value.waveformOpacity,\n \"config.waveformOpacity\",\n waveformOpacityLimits.min,\n waveformOpacityLimits.max,\n errors,\n );\n }\n\n for (const key of [\n \"waveformPlayedStateEnabled\",\n \"initialShuffleEnabled\",\n ] as const) {\n if (has(key)) {\n validateBoolean(value[key], `config.${key}`, errors);\n }\n }\n\n const skipLimits = PLAYER_EXPERIENCE_CONFIG_LIMITS.podcastSkipSec;\n for (const key of [\"podcastBackSec\", \"podcastForwardSec\"] as const) {\n if (has(key)) {\n const candidate = value[key];\n const valid = validateFiniteRange(\n candidate,\n `config.${key}`,\n skipLimits.minExclusive,\n skipLimits.max,\n errors,\n );\n if (valid && candidate <= skipLimits.minExclusive) {\n errors.push(`config.${key} must be greater than 0.`);\n }\n }\n }\n\n if (has(\"fadeCurve\") && !FADE_CURVES.has(value.fadeCurve as FadeCurve)) {\n errors.push(\"config.fadeCurve is not supported.\");\n }\n if (\n has(\"seekCommitMode\") &&\n !SEEK_COMMIT_MODES.has(value.seekCommitMode as SeekCommitMode)\n ) {\n errors.push(\"config.seekCommitMode is not supported.\");\n }\n if (\n has(\"initialRepeatMode\") &&\n !REPEAT_MODES.has(value.initialRepeatMode as RepeatMode)\n ) {\n errors.push(\"config.initialRepeatMode is not supported.\");\n }\n if (has(\"persistMode\") && !PERSIST_MODES.has(value.persistMode as PersistMode)) {\n errors.push(\"config.persistMode is not supported.\");\n }\n if (\n has(\"podcastPreviousMode\") &&\n !PODCAST_PREVIOUS_MODES.has(\n value.podcastPreviousMode as PodcastPreviousMode,\n )\n ) {\n errors.push(\"config.podcastPreviousMode is not supported.\");\n }\n\n if (errors.length > 0) {\n return { ok: false, errors: Object.freeze(errors) };\n }\n\n const merged = options.partial\n ? { ...PREVIEW_PLAYER_EXPERIENCE_CONFIG, ...value }\n : value;\n\n return {\n ok: true,\n errors: [],\n value: Object.freeze({ ...merged }) as Readonly<PlayerExperienceConfig>,\n };\n}\n\nexport function validatePlayerExperienceConfig(\n value: unknown,\n): PlayerExperienceConfigValidationResult {\n return validateConfigRecord(value, { partial: false });\n}\n\nexport function validatePlayerExperienceConfigOverride(\n value: unknown,\n): PlayerExperienceConfigValidationResult {\n return validateConfigRecord(value, { partial: true });\n}\n\nexport function resolvePlayerExperienceConfig(\n override?: Partial<PlayerExperienceConfig>,\n): PlayerExperienceConfigValidationResult {\n return validatePlayerExperienceConfigOverride(override ?? {});\n}\n\nexport function parsePlayerExperienceConfigJson(\n json: string,\n): PlayerExperienceConfigValidationResult {\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(json) as unknown;\n } catch {\n return { ok: false, errors: [\"Config JSON is not valid JSON.\"] };\n }\n\n if (!isRecord(parsed)) {\n return { ok: false, errors: [\"Config JSON envelope must be an object.\"] };\n }\n\n const envelopeKeys = Object.keys(parsed);\n const errors: string[] = [];\n\n for (const key of envelopeKeys) {\n if (key !== \"schemaVersion\" && key !== \"config\") {\n errors.push(`envelope.${key} is not supported.`);\n }\n }\n if (!Object.hasOwn(parsed, \"schemaVersion\")) {\n errors.push(\"envelope.schemaVersion is required.\");\n } else if (\n parsed.schemaVersion !== PLAYER_EXPERIENCE_CONFIG_SCHEMA_VERSION\n ) {\n errors.push(\"envelope.schemaVersion is not supported.\");\n }\n if (!Object.hasOwn(parsed, \"config\")) {\n errors.push(\"envelope.config is required.\");\n }\n\n if (errors.length > 0) {\n return { ok: false, errors: Object.freeze(errors) };\n }\n\n return validatePlayerExperienceConfig(parsed.config);\n}\n\nexport function serializePlayerExperienceConfig(\n config: PlayerExperienceConfig,\n): string {\n const result = validatePlayerExperienceConfig(config);\n\n if (!result.ok) {\n throw new TypeError(result.errors.join(\" \"));\n }\n\n return JSON.stringify(\n {\n schemaVersion: PLAYER_EXPERIENCE_CONFIG_SCHEMA_VERSION,\n config: result.value,\n },\n null,\n 2,\n );\n}\n","import type {\n PlayerExperienceConfig,\n PlayerState,\n} from \"../contracts/player\";\nimport { resolvePlayerExperienceConfig } from \"./validation\";\n\nexport function createInitialPlayerState(\n experienceConfig?: Partial<PlayerExperienceConfig>,\n): Readonly<PlayerState> {\n const resolved = resolvePlayerExperienceConfig(experienceConfig);\n\n if (!resolved.ok) {\n throw new TypeError(resolved.errors.join(\" \"));\n }\n\n return Object.freeze({\n currentItem: null,\n currentItemId: null,\n currentQueueItemId: null,\n mediaKind: null,\n status: \"idle\",\n currentTimeSec: 0,\n durationSec: null,\n volume: resolved.value.initialVolume,\n muted: false,\n shuffleEnabled: resolved.value.initialShuffleEnabled,\n repeatMode: resolved.value.initialRepeatMode,\n queue: Object.freeze([]),\n currentQueueIndex: null,\n canSeek: false,\n canGoPrevious: false,\n canGoNext: false,\n needsUserGesture: false,\n lastTransitionCause: null,\n error: null,\n experienceConfig: resolved.value,\n });\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import { Q as QueueItem, q as PlayerMediaAdapter } from '../player-DE9Vqztu.js';
2
+
3
+ interface PlayerQueueCandidate<TSource> {
4
+ /** Host-owned stable identity for this occurrence in a queue context. */
5
+ readonly queueItemId: string;
6
+ readonly source: TSource;
7
+ }
8
+ type PlayerQueueRejectionReason = "not-playable" | "duplicate-queue-item-id";
9
+ interface PlayerQueueRejection {
10
+ readonly candidateIndex: number;
11
+ readonly queueItemId: string;
12
+ readonly reason: PlayerQueueRejectionReason;
13
+ }
14
+ interface PlayerQueueBuildResult {
15
+ readonly queue: readonly QueueItem[];
16
+ readonly rejected: readonly PlayerQueueRejection[];
17
+ }
18
+ /**
19
+ * Builds a Core queue from consumer records that already passed Host access
20
+ * policy. Candidate order is preserved. The same media item may occur more
21
+ * than once when each occurrence has a unique queueItemId.
22
+ *
23
+ * The result retains only Player items, never the consumer record itself.
24
+ */
25
+ declare function buildPlayerQueue<TSource>(candidates: readonly PlayerQueueCandidate<TSource>[], adapter: PlayerMediaAdapter<TSource>): PlayerQueueBuildResult;
26
+
27
+ export { type PlayerQueueBuildResult, type PlayerQueueCandidate, type PlayerQueueRejection, type PlayerQueueRejectionReason, buildPlayerQueue };
@@ -0,0 +1,38 @@
1
+ // src/consumer/queue.ts
2
+ function buildPlayerQueue(candidates, adapter) {
3
+ const queue = [];
4
+ const rejected = [];
5
+ const seenQueueItemIds = /* @__PURE__ */ new Set();
6
+ candidates.forEach((candidate, candidateIndex) => {
7
+ if (seenQueueItemIds.has(candidate.queueItemId)) {
8
+ rejected.push(Object.freeze({
9
+ candidateIndex,
10
+ queueItemId: candidate.queueItemId,
11
+ reason: "duplicate-queue-item-id"
12
+ }));
13
+ return;
14
+ }
15
+ seenQueueItemIds.add(candidate.queueItemId);
16
+ const item = adapter.toPlayerMediaItem(candidate.source);
17
+ if (item === null) {
18
+ rejected.push(Object.freeze({
19
+ candidateIndex,
20
+ queueItemId: candidate.queueItemId,
21
+ reason: "not-playable"
22
+ }));
23
+ return;
24
+ }
25
+ queue.push(Object.freeze({
26
+ queueItemId: candidate.queueItemId,
27
+ item
28
+ }));
29
+ });
30
+ return Object.freeze({
31
+ queue: Object.freeze(queue),
32
+ rejected: Object.freeze(rejected)
33
+ });
34
+ }
35
+
36
+ export { buildPlayerQueue };
37
+ //# sourceMappingURL=index.js.map
38
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/consumer/queue.ts"],"names":[],"mappings":";AAiCO,SAAS,gBAAA,CACd,YACA,OAAA,EACwB;AACxB,EAAA,MAAM,QAAqB,EAAC;AAC5B,EAAA,MAAM,WAAmC,EAAC;AAC1C,EAAA,MAAM,gBAAA,uBAAuB,GAAA,EAAY;AAEzC,EAAA,UAAA,CAAW,OAAA,CAAQ,CAAC,SAAA,EAAW,cAAA,KAAmB;AAChD,IAAA,IAAI,gBAAA,CAAiB,GAAA,CAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAC/C,MAAA,QAAA,CAAS,IAAA,CAAK,OAAO,MAAA,CAAO;AAAA,QAC1B,cAAA;AAAA,QACA,aAAa,SAAA,CAAU,WAAA;AAAA,QACvB,MAAA,EAAQ;AAAA,OACT,CAAC,CAAA;AACF,MAAA;AAAA,IACF;AACA,IAAA,gBAAA,CAAiB,GAAA,CAAI,UAAU,WAAW,CAAA;AAE1C,IAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,iBAAA,CAAkB,SAAA,CAAU,MAAM,CAAA;AACvD,IAAA,IAAI,SAAS,IAAA,EAAM;AACjB,MAAA,QAAA,CAAS,IAAA,CAAK,OAAO,MAAA,CAAO;AAAA,QAC1B,cAAA;AAAA,QACA,aAAa,SAAA,CAAU,WAAA;AAAA,QACvB,MAAA,EAAQ;AAAA,OACT,CAAC,CAAA;AACF,MAAA;AAAA,IACF;AAEA,IAAA,KAAA,CAAM,IAAA,CAAK,OAAO,MAAA,CAAO;AAAA,MACvB,aAAa,SAAA,CAAU,WAAA;AAAA,MACvB;AAAA,KACD,CAAC,CAAA;AAAA,EACJ,CAAC,CAAA;AAED,EAAA,OAAO,OAAO,MAAA,CAAO;AAAA,IACnB,KAAA,EAAO,MAAA,CAAO,MAAA,CAAO,KAAK,CAAA;AAAA,IAC1B,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,QAAQ;AAAA,GACjC,CAAA;AACH","file":"index.js","sourcesContent":["import type {\n PlayerMediaAdapter,\n QueueItem,\n} from \"../contracts/player\";\n\nexport interface PlayerQueueCandidate<TSource> {\n /** Host-owned stable identity for this occurrence in a queue context. */\n readonly queueItemId: string;\n readonly source: TSource;\n}\n\nexport type PlayerQueueRejectionReason =\n | \"not-playable\"\n | \"duplicate-queue-item-id\";\n\nexport interface PlayerQueueRejection {\n readonly candidateIndex: number;\n readonly queueItemId: string;\n readonly reason: PlayerQueueRejectionReason;\n}\n\nexport interface PlayerQueueBuildResult {\n readonly queue: readonly QueueItem[];\n readonly rejected: readonly PlayerQueueRejection[];\n}\n\n/**\n * Builds a Core queue from consumer records that already passed Host access\n * policy. Candidate order is preserved. The same media item may occur more\n * than once when each occurrence has a unique queueItemId.\n *\n * The result retains only Player items, never the consumer record itself.\n */\nexport function buildPlayerQueue<TSource>(\n candidates: readonly PlayerQueueCandidate<TSource>[],\n adapter: PlayerMediaAdapter<TSource>,\n): PlayerQueueBuildResult {\n const queue: QueueItem[] = [];\n const rejected: PlayerQueueRejection[] = [];\n const seenQueueItemIds = new Set<string>();\n\n candidates.forEach((candidate, candidateIndex) => {\n if (seenQueueItemIds.has(candidate.queueItemId)) {\n rejected.push(Object.freeze({\n candidateIndex,\n queueItemId: candidate.queueItemId,\n reason: \"duplicate-queue-item-id\",\n }));\n return;\n }\n seenQueueItemIds.add(candidate.queueItemId);\n\n const item = adapter.toPlayerMediaItem(candidate.source);\n if (item === null) {\n rejected.push(Object.freeze({\n candidateIndex,\n queueItemId: candidate.queueItemId,\n reason: \"not-playable\",\n }));\n return;\n }\n\n queue.push(Object.freeze({\n queueItemId: candidate.queueItemId,\n item,\n }));\n });\n\n return Object.freeze({\n queue: Object.freeze(queue),\n rejected: Object.freeze(rejected),\n });\n}\n"]}
@@ -0,0 +1,30 @@
1
+ {
2
+ "hoistedDependencies": {
3
+ "scheduler@0.27.0": {
4
+ "scheduler": "private"
5
+ }
6
+ },
7
+ "hoistPattern": [
8
+ "*"
9
+ ],
10
+ "included": {
11
+ "dependencies": true,
12
+ "devDependencies": true,
13
+ "optionalDependencies": true
14
+ },
15
+ "injectedDeps": {},
16
+ "layoutVersion": 5,
17
+ "nodeLinker": "isolated",
18
+ "packageManager": "pnpm@11.9.0",
19
+ "pendingBuilds": [],
20
+ "publicHoistPattern": [],
21
+ "prunedAt": "Sat, 01 Aug 2026 10:23:34 GMT",
22
+ "registries": {
23
+ "default": "https://registry.npmjs.org/",
24
+ "@jsr": "https://npm.jsr.io/"
25
+ },
26
+ "skipped": [],
27
+ "storeDir": "M:\\.pnpm-store\\v11",
28
+ "virtualStoreDir": "M:\\Codex Local\\optimized-web-audio-player\\dist\\consumer-smoke\\node_modules\\.pnpm",
29
+ "virtualStoreDirMaxLength": 60
30
+ }
@@ -0,0 +1 @@
1
+ {"packages":{".":{"url":"..","dependencies":{"@gentomiyano/optimized-web-audio-player":"@gentomiyano/optimized-web-audio-player@file:../gentomiyano-optimized-web-audio-player-0.1.0.tgz(react-dom@19.2.8(react@19.2.8))(react@19.2.8)","player-release-consumer-smoke":".","react":"react@19.2.8","react-dom":"react-dom@19.2.8(react@19.2.8)"}},"@gentomiyano/optimized-web-audio-player@file:../gentomiyano-optimized-web-audio-player-0.1.0.tgz(react-dom@19.2.8(react@19.2.8))(react@19.2.8)":{"url":"./.pnpm/@gentomiyano+optimized-web-_ecc6545b5453980ce895c6698641da69/node_modules/@gentomiyano/optimized-web-audio-player","dependencies":{"@gentomiyano/optimized-web-audio-player":"@gentomiyano/optimized-web-audio-player@file:../gentomiyano-optimized-web-audio-player-0.1.0.tgz(react-dom@19.2.8(react@19.2.8))(react@19.2.8)","react":"react@19.2.8","react-dom":"react-dom@19.2.8(react@19.2.8)"}},"react-dom@19.2.8(react@19.2.8)":{"url":"./.pnpm/react-dom@19.2.8_react@19.2.8/node_modules/react-dom","dependencies":{"react":"react@19.2.8","react-dom":"react-dom@19.2.8(react@19.2.8)","scheduler":"scheduler@0.27.0"}},"react@19.2.8":{"url":"./.pnpm/react@19.2.8/node_modules/react","dependencies":{"react":"react@19.2.8"}},"scheduler@0.27.0":{"url":"./.pnpm/scheduler@0.27.0/node_modules/scheduler","dependencies":{"scheduler":"scheduler@0.27.0"}}}}