diff --git a/configs/vitest.config.ts b/configs/vitest.config.ts index 33c93a4e4..e31dd4ceb 100644 --- a/configs/vitest.config.ts +++ b/configs/vitest.config.ts @@ -53,6 +53,9 @@ export default defineConfig(({ mode }) => { }), }, resolve: { + alias: { + "solid-js/web": "@solidjs/web", + }, conditions: testSSR ? ["@solid-primitives/source", "node"] : ["@solid-primitives/source", "browser", "development"], diff --git a/package.json b/package.json index a2614f8c7..7db1b1b47 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,8 @@ "rehype-highlight": "^7.0.2", "rehype-slug": "^6.0.0", "remark-gfm": "^4.0.1", - "solid-js": "^1.9.7", + "@solidjs/web": "^2.0.0-experimental.16", + "solid-js": "^2.0.0-beta.6", "typescript": "^5.8.3", "vinxi": "^0.5.7", "vite": "^6.3.5", 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/README.md b/packages/audio/README.md index 45169159c..0ad192645 100644 --- a/packages/audio/README.md +++ b/packages/audio/README.md @@ -8,9 +8,9 @@ [![size](https://img.shields.io/npm/v/@solid-primitives/audio?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/audio) [![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-3.json)](https://github.com/solidjs-community/solid-primitives#contribution-process) -Primitive to manage audio playback in the browser. The primitives are easily composable and extended. To create your own audio element, consider using makeAudioPlayer which allows you to supply a player instance that matches the built-in standard Audio API. +Primitives to manage audio playback in the browser. The primitives are layered: `make*` variants are non-reactive base primitives that require no Solid owner, while `createAudio` integrates with Solid's reactive system. -Each primitive also exposes the audio instance for further custom extensions. Within an SSR context this audio primitive performs noops but never interrupts the process. Time values and durations are zero'd and in LOADING state. +Within an SSR context these primitives perform noops and never interrupt the process. ## Installation @@ -24,34 +24,48 @@ yarn add @solid-primitives/audio ### makeAudio -A foundational primitive with no player controls but exposes the raw player object. +A foundational non-reactive primitive that creates a raw `HTMLAudioElement` with optional event handlers. No Solid owner required. ```ts -const player = makeAudio("example.mp3"); +const [player, cleanup] = makeAudio("example.mp3"); +// later: +cleanup(); ``` #### Definition ```ts -function makeAudio(src: AudioSource, handlers: AudioEventHandlers = {}): HTMLAudioElement; +function makeAudio( + src: AudioSource | HTMLAudioElement, + handlers?: AudioEventHandlers, +): [player: HTMLAudioElement, cleanup: VoidFunction]; ``` ### makeAudioPlayer -Provides a very basic interface for wrapping listeners to a supplied or default audio player. +Wraps `makeAudio` with simple playback controls. No Solid owner required. ```ts -const { play, pause, seek } = makeAudioPlayer("example.mp3"); +const [{ play, pause, seek, setVolume, player }, cleanup] = makeAudioPlayer("example.mp3"); +play(); +seek(30); +cleanup(); ``` #### Definition ```ts function makeAudioPlayer( - src: AudioSource, - handlers: AudioEventHandlers = {}, -): { - play: VoidFunction; + src: AudioSource | HTMLAudioElement, + handlers?: AudioEventHandlers, +): [controls: AudioControls, cleanup: VoidFunction]; +``` + +`AudioControls`: + +```ts +type AudioControls = { + play: () => Promise; pause: VoidFunction; seek: (time: number) => void; setVolume: (volume: number) => void; @@ -59,67 +73,75 @@ function makeAudioPlayer( }; ``` -The seek function falls back to fastSeek when on [supporting browsers](https://caniuse.com/?search=fastseek). +The `seek` function uses `fastSeek` on [supporting browsers](https://caniuse.com/?search=fastseek). ### createAudio -Creates a very basic audio/sound player with reactive properties to control the audio. Be careful not to destructure the value properties provided by the primitive as it will likely break reactivity. +A reactive audio primitive. Returns a flat object with writable signal accessors for `playing` and `volume`, a reactive `currentTime`, and an async `duration` that suspends until audio metadata is loaded — integrating with `` / ``. ```ts -const [playing, setPlaying] = createSignal(false); -const [volume, setVolume] = createSignal(false); -const [audio, controls] = createAudio("sample.mp3", playing, volume); -setPlaying(true); // or controls.play() -controls.seek(4000); +const audio = createAudio("example.mp3"); + +audio.playing() // boolean +audio.setPlaying(true) // plays +audio.volume() // 0–1 +audio.setVolume(0.5) +audio.currentTime() // seconds +audio.seek(30) ``` -The audio primitive exports an reactive properties that provides you access to state, duration and current time. +The `duration` accessor returns a Promise, so wrap it in ``: + +```tsx + + {audio.duration()}s + +``` -_Note:_ Initializing the primitive with `playing` as true works, however note that the user has to interact with the page first (on a fresh page load). +The `src` argument can be a reactive accessor — switching sources replaces the track and seeks to the start: ```ts -function createAudio( - src: AudioSource | Accessor, - playing?: Accessor, - volume?: Accessor, -): [ - { - state: AudioState; - currentTime: number; - duration: number; - volume: number; - player: HTMLAudioElement; - }, - { - seek: (time: number) => void; - setVolume: (volume: number) => void; - play: VoidFunction; - pause: VoidFunction; - }, -]; +const [src, setSrc] = createSignal("track1.mp3"); +const audio = createAudio(src); +setSrc("track2.mp3"); ``` -#### Dynamic audio changes +#### Definition + +```ts +function createAudio(src: AudioSource | Accessor): AudioReturn; +``` -The source property can be a signal as well as a media source. Upon switching the source via a signal it will continue playing from the head. +`AudioReturn`: ```ts -const [src, setSrc] = createSignal("sample.mp3"); -const audio = createAudio(src); -setSrc("sample2.mp3"); +type AudioReturn = { + player: HTMLAudioElement; + playing: Accessor; + setPlaying: (v: boolean) => void; + volume: Accessor; + setVolume: (v: number) => void; + currentTime: Accessor; + duration: Accessor; // async — suspends until loaded + seek: (time: number) => void; +}; ``` -### Audio Source +## Audio Source -`createAudio` as well as `makeAudio` and `makeAudioPlayer` all accept MediaSource as a property. +All primitives accept `AudioSource` as their `src` argument: + +```ts +type AudioSource = string | undefined | MediaProvider; +``` + +This includes `MediaSource` and `MediaStream`, enabling streamed or Blob-backed audio: ```ts const media = new MediaSource(); const audio = createAudio(URL.createObjectURL(media)); ``` -This allows you to managed streamed or Blob supplied media. In essence the primitives in this package are very flexible and allow direct access to the base browser API. - ## Demo You may view a working example here: https://stackblitz.com/edit/vitejs-vite-zwfs6h?file=src%2Fmain.tsx 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]) => (