From 4f52a74dd8c806e3f0ad1ce6dbd676986249b7f5 Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:10:35 -0400 Subject: [PATCH 1/5] Initial work on adapting audio primitive --- packages/audio/package.json | 4 +- packages/audio/src/index.ts | 46 +++++++++++++--------- packages/audio/test/index.test.ts | 28 ++++++++++++- pnpm-lock.yaml | 65 +++++++++++++++++++++++-------- site/app.config.ts | 3 ++ 5 files changed, 106 insertions(+), 40 deletions(-) diff --git a/packages/audio/package.json b/packages/audio/package.json index 172db3727..50406622f 100644 --- a/packages/audio/package.json +++ b/packages/audio/package.json @@ -50,13 +50,13 @@ "primitives" ], "devDependencies": { - "solid-js": "^1.9.7" + "solid-js": "^2.0.0-beta.6" }, "dependencies": { "@solid-primitives/static-store": "workspace:^", "@solid-primitives/utils": "workspace:^" }, "peerDependencies": { - "solid-js": "^1.6.12" + "solid-js": "^2.0.0-beta.6" } } diff --git a/packages/audio/src/index.ts b/packages/audio/src/index.ts index a81853460..e6158a314 100644 --- a/packages/audio/src/index.ts +++ b/packages/audio/src/index.ts @@ -1,4 +1,4 @@ -import { type Accessor, onMount, onCleanup, createEffect } from "solid-js"; +import { type Accessor, onSettled, createEffect } from "solid-js"; import { isServer } from "solid-js/web"; import { access, noop } from "@solid-primitives/utils"; import { createStaticStore } from "@solid-primitives/static-store"; @@ -56,16 +56,16 @@ export const makeAudio = ( const player = unwrapSource(src); - onMount(() => { + onSettled(() => { for (const [name, handler] of Object.entries(handlers)) { player.addEventListener(name, handler as any); } - }); - onCleanup(() => { - player.pause(); - for (const [name, handler] of Object.entries(handlers)) { - player.removeEventListener(name, handler as any); - } + return () => { + player.pause(); + for (const [name, handler] of Object.entries(handlers)) { + player.removeEventListener(name, handler as any); + } + }; }); return player; @@ -228,22 +228,30 @@ export const createAudio = ( // Bind reactive properties as needed if (src instanceof Function) { - createEffect(() => { - const newSrc = src(); - if (newSrc instanceof HTMLAudioElement) { - setStore("player", newSrc); - } else { - setAudioSrc(store.player, newSrc); - } - seek(0); - }); + createEffect( + () => src(), + (newSrc) => { + if (newSrc instanceof HTMLAudioElement) { + setStore("player", newSrc); + } else { + setAudioSrc(store.player, newSrc); + } + seek(0); + }, + ); } if (playing) { - createEffect(() => (playing() ? play() : pause())); + createEffect( + () => playing(), + (isPlaying) => (isPlaying ? play() : pause()), + ); } if (volume) { - createEffect(() => setVolume(volume())); + createEffect( + () => volume(), + (vol) => setVolume(vol), + ); setVolume(volume()); } diff --git a/packages/audio/test/index.test.ts b/packages/audio/test/index.test.ts index c8e4fede9..bf4d82393 100644 --- a/packages/audio/test/index.test.ts +++ b/packages/audio/test/index.test.ts @@ -6,6 +6,9 @@ import { makeAudio, makeAudioPlayer, createAudio, AudioState } from "../src/inde const testPath = "https://github.com/solidjs-community/solid-primitives/blob/audio/packages/audio/dev/sample1.mp3?raw=true"; +/** Yield to the microtask queue twice — enough for Solid 2.0's split compute/apply effect model. */ +const tick = () => Promise.resolve().then(() => Promise.resolve()); + describe("makeAudio", () => { it("test static string path", () => createRoot(dispose => { @@ -65,10 +68,12 @@ describe("createAudio", () => { const [audio] = createAudio("test.mp3", playing, volume); audio.player._mock._load(audio.player); expect(audio.player._mock.paused).toBe(true); - await setPlaying(true); + setPlaying(true); + await tick(); expect(audio.player._mock.paused).toBe(false); expect(audio.player.volume).toBe(0.25); - await setVolume(0.5); + setVolume(0.5); + await tick(); expect(audio.player.volume).toBe(0.5); dispose(); })); @@ -82,4 +87,23 @@ describe("createAudio", () => { dispose(); }); + + it("initial volume is applied synchronously", () => + createRoot(dispose => { + const [volume] = createSignal(0.5); + const [audio] = createAudio("test.mp3", undefined, volume); + expect(audio.player.volume).toBe(0.5); + dispose(); + })); + + it("src signal change updates player source", () => + createRoot(async dispose => { + const [src, setSrc] = createSignal("track1.mp3"); + const [audio] = createAudio(src); + expect(audio.player.src).toBe("track1.mp3"); + setSrc("track2.mp3"); + await tick(); + expect(audio.player.src).toBe("track2.mp3"); + dispose(); + })); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ecadfdb95..e1acfc00e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,8 +116,8 @@ importers: version: link:../utils devDependencies: solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: ^2.0.0-beta.6 + version: 2.0.0-experimental.16 packages/autofocus: dependencies: @@ -1048,10 +1048,10 @@ importers: version: link:../packages/utils '@solidjs/meta': specifier: ^0.29.3 - version: 0.29.4(solid-js@1.9.7) + version: 0.29.4(solid-js@2.0.0-experimental.16) '@solidjs/router': specifier: ^0.13.1 - version: 0.13.6(solid-js@1.9.7) + version: 0.13.6(solid-js@2.0.0-experimental.16) clsx: specifier: ^2.0.0 version: 2.1.1 @@ -1078,13 +1078,13 @@ importers: version: 1.77.8 solid-dismiss: specifier: ^1.7.121 - version: 1.8.2(solid-js@1.9.7) + version: 1.8.2(solid-js@2.0.0-experimental.16) solid-icons: specifier: ^1.1.0 - version: 1.1.0(solid-js@1.9.7) + version: 1.1.0(solid-js@2.0.0-experimental.16) solid-tippy: specifier: ^0.2.1 - version: 0.2.1(solid-js@1.9.7)(tippy.js@6.3.7) + version: 0.2.1(solid-js@2.0.0-experimental.16)(tippy.js@6.3.7) tippy.js: specifier: ^6.3.7 version: 6.3.7 @@ -2587,6 +2587,9 @@ packages: peerDependencies: solid-js: ^1.5.3 + '@solidjs/signals@0.11.3': + resolution: {integrity: sha512-udMfutYPOlcxKUmc5+n1QtarsxOiAlC6LJY2TqFyaMwdXgo+reiYUcYGDlOiAPXfCLE0lavZHQ/6GT5pJbXKBA==} + '@solidjs/start@1.1.4': resolution: {integrity: sha512-ma1TBYqoTju87tkqrHExMReM5Z/+DTXSmi30CCTavtwuR73Bsn4rVGqm528p4sL2koRMfAuBMkrhuttjzhL68g==} peerDependencies: @@ -5892,10 +5895,20 @@ packages: peerDependencies: seroval: ^1.0 + seroval-plugins@1.5.2: + resolution: {integrity: sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + seroval@1.3.2: resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} + seroval@1.5.2: + resolution: {integrity: sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==} + engines: {node: '>=10'} + serve-placeholder@2.0.2: resolution: {integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==} @@ -6001,6 +6014,9 @@ packages: solid-js@1.9.7: resolution: {integrity: sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==} + solid-js@2.0.0-experimental.16: + resolution: {integrity: sha512-zZ1dU7cR0EnvLnrYiRLQbCFiDw5blLdlqmofgLzKUYE1TCMWDcisBlSwz0Ez8l4yXB4adbdhtaYCuynH4xSq9A==} + solid-refresh@0.6.3: resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} peerDependencies: @@ -8576,18 +8592,20 @@ snapshots: dependencies: solid-js: 1.9.7 - '@solidjs/meta@0.29.4(solid-js@1.9.7)': + '@solidjs/meta@0.29.4(solid-js@2.0.0-experimental.16)': dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-experimental.16 - '@solidjs/router@0.13.6(solid-js@1.9.7)': + '@solidjs/router@0.13.6(solid-js@2.0.0-experimental.16)': dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-experimental.16 '@solidjs/router@0.8.4(solid-js@1.9.7)': dependencies: solid-js: 1.9.7 + '@solidjs/signals@0.11.3': {} + '@solidjs/start@1.1.4(solid-js@1.9.7)(vinxi@0.5.7(@types/node@22.15.31)(db0@0.3.2)(ioredis@5.6.1)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': dependencies: '@tanstack/server-functions-plugin': 1.121.0(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) @@ -12441,8 +12459,14 @@ snapshots: dependencies: seroval: 1.3.2 + seroval-plugins@1.5.2(seroval@1.5.2): + dependencies: + seroval: 1.5.2 + seroval@1.3.2: {} + seroval@1.5.2: {} + serve-placeholder@2.0.2: dependencies: defu: 6.1.4 @@ -12557,13 +12581,13 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - solid-dismiss@1.8.2(solid-js@1.9.7): + solid-dismiss@1.8.2(solid-js@2.0.0-experimental.16): dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-experimental.16 - solid-icons@1.1.0(solid-js@1.9.7): + solid-icons@1.1.0(solid-js@2.0.0-experimental.16): dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-experimental.16 solid-js@1.9.7: dependencies: @@ -12571,6 +12595,13 @@ snapshots: seroval: 1.3.2 seroval-plugins: 1.3.2(seroval@1.3.2) + solid-js@2.0.0-experimental.16: + dependencies: + '@solidjs/signals': 0.11.3 + csstype: 3.1.3 + seroval: 1.5.2 + seroval-plugins: 1.5.2(seroval@1.5.2) + solid-refresh@0.6.3(solid-js@1.9.7): dependencies: '@babel/generator': 7.27.5 @@ -12580,9 +12611,9 @@ snapshots: transitivePeerDependencies: - supports-color - solid-tippy@0.2.1(solid-js@1.9.7)(tippy.js@6.3.7): + solid-tippy@0.2.1(solid-js@2.0.0-experimental.16)(tippy.js@6.3.7): dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-experimental.16 tippy.js: 6.3.7 solid-transition-group@0.2.3(solid-js@1.9.7): diff --git a/site/app.config.ts b/site/app.config.ts index d176ab305..3d04ac4ef 100644 --- a/site/app.config.ts +++ b/site/app.config.ts @@ -18,6 +18,9 @@ export default defineConfig({ build: { sourcemap: true, }, + optimizeDeps: { + exclude: ["fsevents"], + }, }, server: { prerender: { From ab2c0c984f6dabf7c27ce20a3c0db2616482fcc8 Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:35:20 -0400 Subject: [PATCH 2/5] Applied suggestions from issues 721 and 722 --- packages/audio/CHANGELOG.md | 9 ++++ packages/audio/package.json | 2 +- packages/audio/src/index.ts | 82 +++++++++---------------------- packages/audio/test/index.test.ts | 14 ++++-- 4 files changed, 45 insertions(+), 62 deletions(-) diff --git a/packages/audio/CHANGELOG.md b/packages/audio/CHANGELOG.md index 449e19013..85c011612 100644 --- a/packages/audio/CHANGELOG.md +++ b/packages/audio/CHANGELOG.md @@ -1,5 +1,14 @@ # @solid-primitives/audio +## 2.0.0 + +### Major Changes + +- Updated to Solid.js 2.0 API: + - Replaced `onMount` + `onCleanup` with `onSettled` (returning cleanup function) + - Migrated `createEffect` calls to the split compute/apply model + - Updated peer dependency to `solid-js@^2.0.0` + ## 1.4.4 ### Patch Changes diff --git a/packages/audio/package.json b/packages/audio/package.json index 50406622f..35dba35e3 100644 --- a/packages/audio/package.json +++ b/packages/audio/package.json @@ -1,6 +1,6 @@ { "name": "@solid-primitives/audio", - "version": "1.4.4", + "version": "2.0.0", "description": "Primitives to manage audio and single sounds.", "author": "David Di Biase ", "license": "MIT", diff --git a/packages/audio/src/index.ts b/packages/audio/src/index.ts index e6158a314..b0d08e7aa 100644 --- a/packages/audio/src/index.ts +++ b/packages/audio/src/index.ts @@ -14,40 +14,37 @@ export enum AudioState { ERROR = "error", } -export type AudioSource = - | string - | undefined - | HTMLAudioElement - | MediaSource - | (string & MediaSource); +export type AudioSource = string | undefined | MediaProvider; export type AudioEventHandlers = { [K in keyof HTMLMediaElementEventMap]?: (event: HTMLMediaElementEventMap[K]) => void; }; // Helper for producing the audio source -const unwrapSource = (src: AudioSource) => { - if (src instanceof HTMLAudioElement) { - return src; - } +const unwrapSource = (src: AudioSource | HTMLAudioElement) => { + if (src instanceof HTMLAudioElement) return src; const player = new Audio(); setAudioSrc(player, src); return player; }; function setAudioSrc(el: HTMLAudioElement, src: AudioSource) { - el[typeof src === "string" ? "src" : "srcObject"] = src as string & MediaSource; + if (typeof src === "string") { + el.src = src; + } else { + el.srcObject = src as MediaProvider | null; + } } /** * Generates a basic audio instance with limited functionality. * - * @param src Audio file path or MediaSource to be played - * @param handlers An array of handlers to bind against the player + * @param src Audio file path, MediaProvider, or existing HTMLAudioElement + * @param handlers Event handlers to bind against the player * @return A basic audio player instance */ export const makeAudio = ( - src: AudioSource, + src: AudioSource | HTMLAudioElement, handlers: AudioEventHandlers = {}, ): HTMLAudioElement => { if (isServer) { @@ -74,22 +71,16 @@ export const makeAudio = ( /** * Generates a basic audio player with simple control mechanisms. * - * @param src Audio file path or MediaSource to be played - * @return options - @type Object - * @return options.start - Start playing - * @return options.pause - Pause playing - * @return options.seek - Seeks to a location in the playhead - * @return options.setVolume - Sets the volume of the player - * @return options.player - Raw player instance - * @return Returns a location signal and one-off async query callback + * @param src Audio file path, MediaProvider, or existing HTMLAudioElement + * @param handlers Event handlers to bind against the player * * @example * ```ts - * const { start, seek } = makeAudioPlayer('./example1.mp3'); + * const { play, seek } = makeAudioPlayer('./example1.mp3'); * ``` */ export const makeAudioPlayer = ( - src: AudioSource, + src: AudioSource | HTMLAudioElement, handlers: AudioEventHandlers = {}, ): { play: () => Promise; @@ -123,21 +114,9 @@ export const makeAudioPlayer = ( /** * A reactive audio primitive with basic control actions. * - * @param src Audio source path or MediaSource to be played or an accessor - * @param playing A signal for controlling the player - * @param volume A signal for controlling the volume - * @return [store] - @type Store - * @return [store.state] - Current state of the player - * @return [store.currentTime] - Current time of the playhead - * @return [store.duration] - Duration of the loaded file - * @return [store.volume] - Current volume of the audio player - * @return [store.player] - Raw player instance - * @return [controls] - Controls for the audio player @type Object - * @return [controls.seek] - Seeks to a specified location - * @return [controls.play] - Start playing - * @return [controls.pause] - Pause playing - * @return [controls.setVolume] - Sets the volume of the player, from 0 to 1 - * + * @param src Audio file path or MediaProvider, or a reactive accessor returning either + * @param playing Optional signal controlling play/pause state + * @param volume Optional signal controlling volume (0–1) * * @example * ```ts @@ -228,30 +207,17 @@ export const createAudio = ( // Bind reactive properties as needed if (src instanceof Function) { - createEffect( - () => src(), - (newSrc) => { - if (newSrc instanceof HTMLAudioElement) { - setStore("player", newSrc); - } else { - setAudioSrc(store.player, newSrc); - } - seek(0); - }, - ); + createEffect(() => { + setAudioSrc(store.player, src()); + seek(0); + }); } if (playing) { - createEffect( - () => playing(), - (isPlaying) => (isPlaying ? play() : pause()), - ); + createEffect(() => (playing() ? play() : pause())); } if (volume) { - createEffect( - () => volume(), - (vol) => setVolume(vol), - ); + createEffect(() => setVolume(volume())); setVolume(volume()); } diff --git a/packages/audio/test/index.test.ts b/packages/audio/test/index.test.ts index bf4d82393..5746dccb3 100644 --- a/packages/audio/test/index.test.ts +++ b/packages/audio/test/index.test.ts @@ -44,16 +44,24 @@ describe("makeAudioPlayer", () => { it("test srcObject value path", () => createRoot(dispose => { - const { player } = makeAudioPlayer({} as MediaSource); + const { player } = makeAudioPlayer({} as MediaProvider); expect(typeof player.srcObject).toBe("object"); dispose(); })); + + it("accepts MediaStream as source (issue #721)", () => + createRoot(dispose => { + const stream = {} as MediaStream; + const { player } = makeAudioPlayer(stream); + expect(player.srcObject).toBe(stream); + dispose(); + })); }); describe("createAudio", () => { it("test srcObject value path", () => createRoot(dispose => { - const media = {} as MediaSource; + const media = {} as MediaProvider; let [audio] = createAudio(media); expect(typeof audio.player.srcObject).toBe("object"); [audio] = createAudio(() => media); @@ -79,7 +87,7 @@ describe("createAudio", () => { })); it("should set the COMPLETE state when audio ends", () => { - const [[audio], dispose] = createRoot(dispose => [createAudio({} as MediaSource), dispose]); + const [[audio], dispose] = createRoot(dispose => [createAudio({} as MediaProvider), dispose]); expect(audio.state).toBe(AudioState.LOADING); audio.player.dispatchEvent(new Event("ended")); From 6ae3594ffec902f46f5532bd388429f7069cabbf Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:51:34 -0400 Subject: [PATCH 3/5] Simplification and cleanup --- packages/audio/dev/index.tsx | 145 +++++++-------- packages/audio/src/index.ts | 285 +++++++++++++++--------------- packages/audio/test/index.test.ts | 200 +++++++++++++-------- 3 files changed, 336 insertions(+), 294 deletions(-) diff --git a/packages/audio/dev/index.tsx b/packages/audio/dev/index.tsx index b9dea4595..e4d1d19ca 100644 --- a/packages/audio/dev/index.tsx +++ b/packages/audio/dev/index.tsx @@ -1,9 +1,5 @@ -import { type Component, For, type JSX, Show, createSignal, splitProps } from "solid-js"; -import { createAudio, AudioState } from "../src/index.js"; - -// import { Icon } from "solid-heroicons"; -// import { play, pause } from "solid-heroicons/solid"; -// import { speakerWave } from "solid-heroicons/outline"; +import { type Component, For, type JSX, Suspense, createSignal, splitProps } from "solid-js"; +import { createAudio } from "../src/index.js"; type IconPath = { path: () => JSX.Element; outline?: boolean; mini?: boolean }; type IconProps = JSX.SvgSVGAttributes & { path: IconPath }; @@ -25,93 +21,88 @@ const Icon = (props: IconProps) => { const play: IconPath = { path: () => ( - <> - - + ), - outline: false, - mini: false, }; const pause: IconPath = { path: () => ( - <> - - + ), - outline: false, - mini: false, }; const speakerWave: IconPath = { path: () => ( - <> - - + ), outline: true, - mini: false, }; const formatTime = (time: number) => new Date(time * 1000).toISOString().substr(14, 8); +const Player: Component<{ source: () => string }> = props => { + const audio = createAudio(props.source); + + return ( +
+ +
+ + {formatTime(audio.currentTime())} / {formatTime(audio.duration())} + +
+ }> + audio.seek(+evt.currentTarget.value)} + type="range" + min="0" + step="0.1" + max={audio.duration()} + value={audio.currentTime()} + class="form-range w-40 cursor-pointer appearance-none rounded-3xl bg-gray-200 transition hover:bg-gray-300 focus:ring-0 focus:outline-none" + /> + +
+ + audio.setVolume(+evt.currentTarget.value)} + type="range" + min="0" + step="0.1" + max={1} + value={audio.volume()} + class="cursor w-10" + /> +
+
+ ); +}; + const App: Component = () => { const [source, setSource] = createSignal("sample1.mp3"); - const [playing, setPlaying] = createSignal(false); - const [volume, setVolume] = createSignal(1); - const [audio, { seek }] = createAudio(source, playing, volume); return (
-
- -
- - {formatTime(audio.currentTime)} / {formatTime(audio.duration)} - -
- seek(+evt.currentTarget.value)} - type="range" - min="0" - step="0.1" - max={audio.duration} - value={audio.currentTime} - class="form-range w-40 cursor-pointer appearance-none rounded-3xl bg-gray-200 transition hover:bg-gray-300 focus:outline-none focus:ring-0" - /> -
- - setVolume(+evt.currentTarget.value)} - type="range" - min="0" - step="0.1" - max={1} - value={volume()} - class="cursor w-10" - /> -
-
+
{ > {([label, url]) => (