Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/mediastream-solid2-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@solid-primitives/mediastream": major
---

Initial release of `@solid-primitives/mediastream`, replacing `@solid-primitives/stream` for Solid.js v2.

## Breaking Changes

**Peer dependency**: `solid-js@^2.0.0-beta.14` and `@solidjs/web@^2.0.0-beta.14` are now required.

### `@solid-primitives/mediastream`

- `createStream` and `createScreen` now return `[Accessor<MediaStream | undefined>, { stop, mute }]` instead of `[Resource<MediaStream>, ResourceActions & { stop, mute }]`. The `mutate` and `refetch` controls are removed — source reactivity drives re-acquisition automatically.
- `createAmplitudeStream` second element is now `{ stream, stop }` (no `mutate` / `refetch`).
- `ResourceActions` type export removed; no longer depends on `createResource`.
- Loading state: use `<Loading>` from `@solidjs/web` to handle the pending state.
- Error state: use `<Errored>` from `@solidjs/web` to handle stream acquisition errors.
- `isServer` imported internally from `@solidjs/web` (not `solid-js/web`).
- All `createEffect` calls use the Solid 2.0 split compute/apply form.
16 changes: 16 additions & 0 deletions packages/mediastream/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# @solid-primitives/mediastream

## 0.1.0

### Major Changes

Initial release as `@solid-primitives/mediastream`, replacing `@solid-primitives/stream` with full Solid.js v2 compatibility.

- Migrated from `createResource` to reactive signals + split `createEffect` for async stream acquisition
- `isServer` imported from `@solidjs/web`
- `createStream` / `createScreen`: return type is now `[Accessor<MediaStream | undefined>, { stop, mute }]` — `mutate` and `refetch` removed
- `createAmplitudeStream`: second element simplified to `{ stream, stop }`
- `createAmplitudeFromStream`: split `createEffect` for stream-to-analyser binding
- Loading state handled via `<Loading>` from `@solidjs/web`; error state via `<Errored>`
- Added `getDisplayMedia` mock support in test setup
- Race conditions between concurrent getUserMedia calls handled with `active` flag
160 changes: 160 additions & 0 deletions packages/mediastream/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<p>
<img width="100%" src="https://assets.solidjs.com/banner?type=Primitives&background=tiles&project=Mediastream" alt="Solid Primitives Mediastream">
</p>

# @solid-primitives/mediastream

[![size](https://img.shields.io/bundlephobia/minzip/@solid-primitives/mediastream?style=for-the-badge)](https://bundlephobia.com/package/@solid-primitives/mediastream)
[![size](https://img.shields.io/npm/v/@solid-primitives/mediastream?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/mediastream)
[![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-0.json)](https://github.com/solidjs-community/solid-primitives#contribution-process)

Reactive primitives for working with [MediaStream](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) — microphones, cameras, and screen capture.

## Installation

```bash
npm install @solid-primitives/mediastream
# or
pnpm add @solid-primitives/mediastream
```

## Primitives

### `createStream`

Creates a reactive accessor for a `MediaStream` from a camera or microphone.

```ts
const [stream, { mute, stop }] = createStream(streamSource);
```

**Parameters**

- `streamSource` — `MediaDeviceInfo | MediaStreamConstraints | Accessor<...> | FalsyValue`

**Returns**

- `stream()` — `Accessor<MediaStream | undefined>` — the current stream (undefined while loading or stopped)
- `mute(muted?)` — mutes the stream; pass `false` to unmute
- `stop()` — stops the stream immediately

The stream stops automatically when the reactive owner is disposed. Wrap in `<Loading>` to handle the loading state:

```tsx
import { Loading } from "@solidjs/web";

const [stream] = createStream({ video: true });

<Loading fallback={<p>Requesting camera...</p>}>
<video ref={el => createEffect(stream, s => { el.srcObject = s ?? null; })} autoplay />
</Loading>
```

### `createScreen`

Creates a reactive accessor for a display capture stream (screen, window, or browser tab).

```ts
const [stream, { mute, stop }] = createScreen(screenSource);
```

**Parameters**

- `screenSource` — `DisplayMediaStreamConstraints | undefined | Accessor<DisplayMediaStreamConstraints | undefined>`

Same controls as `createStream` but uses `getDisplayMedia` instead of `getUserMedia`. The stream stops automatically when the reactive owner is disposed.

```tsx
const [stream] = createScreen({ video: true });

<Loading fallback={<p>Requesting screen capture...</p>}>
<video ref={el => createEffect(stream, s => { el.srcObject = s ?? null; })} autoplay />
</Loading>
```

### `createAmplitudeStream`

Creates a reactive signal with the RMS amplitude (0–100) from a microphone device.

```ts
const [amplitude, { stream, stop }] = createAmplitudeStream(streamSource?);
```

**Parameters**

- `streamSource?` — same as `createStream` (optional)

**Returns**

- `amplitude()` — `Accessor<number>` — value between 0 and 100
- `stream` — `Accessor<MediaStream | undefined>` — the underlying stream
- `stop()` — stops the amplitude measurement and underlying stream

```tsx
const [audioConstraints, setAudioConstraints] = createSignal<MediaStreamConstraints>();
const [level] = createAmplitudeStream(audioConstraints);

<Show
when={audioConstraints()}
fallback={<button onClick={() => setAudioConstraints({ audio: true })}>Start</button>}
>
<meter min="0" max="100" value={level()} />
</Show>
```

### `createAmplitudeFromStream`

Creates an amplitude signal from an existing stream accessor.

```ts
const [amplitude, stop] = createAmplitudeFromStream(stream);
```

**Parameters**

- `stream` — `MaybeAccessor<MediaStream | undefined>`

**Returns**

- `amplitude()` — `Accessor<number>` — value between 0 and 100
- `stop()` — stops the amplitude measurement

The measurement stops automatically when the reactive owner is disposed.

### `createMediaPermissionRequest`

Requests media permissions from the user by briefly opening then immediately stopping a stream.

```ts
createMediaPermissionRequest(source?);
```

**Parameters**

- `source?` — `'audio' | 'video' | MediaStreamConstraints` — defaults to both audio and video

Returns a `Promise<void>` that resolves when permission is granted, and rejects if the user denies permission or `navigator.mediaDevices.getUserMedia` fails for any other reason — forwarding the rejection directly from `getUserMedia`.

```ts
// Request both microphone and camera permissions
await createMediaPermissionRequest();

// Request only microphone permission
await createMediaPermissionRequest('audio');
```

Use `createPermission` from `@solid-primitives/permission` to reactively observe the resulting permission state.

## Breaking Changes from `@solid-primitives/stream`

This package replaces `@solid-primitives/stream` with full Solid.js v2 compatibility.

- **`createStream` / `createScreen`**: return type changed from `[Resource<MediaStream>, ResourceActions & { stop, mute }]` to `[Accessor<MediaStream | undefined>, { stop, mute }]`. The `mutate` and `refetch` controls are removed; reactivity is driven by the source accessor directly.
- **`createAmplitudeStream`**: return type simplified — second element is now `{ stream, stop }` (no `mutate` / `refetch`).
- **`isServer`**: now imported from `@solidjs/web` internally.
- **Async loading state**: use `<Loading>` from `@solidjs/web` instead of inspecting `stream.loading`.
- **Error handling**: wrap in `<Errored>` from `@solidjs/web` instead of inspecting `stream.error`.

## Changelog

See [CHANGELOG.md](./CHANGELOG.md)
61 changes: 61 additions & 0 deletions packages/mediastream/dev/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { Component } from "solid-js";
import { createSignal, Show } from "solid-js";
import type { JSX } from "@solidjs/web";

import {
createStream,
createAmplitudeStream,
createMediaPermissionRequest,
createScreen,
} from "../src/index.js";

declare module "@solidjs/web" {
namespace JSX {
interface ExplicitProperties {
srcObject?: MediaStream;
}
}
}

export type E = JSX.Element;

const App: Component = () => {
createMediaPermissionRequest();
const [video] = createStream({ video: true });
const [audioConstraints, setAudioConstraints] = createSignal<MediaStreamConstraints>();
const [level] = createAmplitudeStream(audioConstraints);
const [screenConstraints, setScreenConstraints] = createSignal<MediaStreamConstraints>();
const [screen] = createScreen(screenConstraints);
return (
<div class="box-border flex min-h-screen w-full flex-col items-center justify-center space-y-4 bg-gray-800 p-24 text-white">
<div class="wrapper-v">
<h2>TestVideo</h2>
<video prop:srcObject={video()} autoplay />
<h2>Amplitude test</h2>
<Show
when={audioConstraints()}
fallback={
<button
onClick={() => setAudioConstraints({ audio: true })}
title="We need user interaction to run an audio context"
>
Click to start amplitude level
</button>
}
>
<meter min="0" max="100" value={level()} />
</Show>
<h2>Screen Capture Test</h2>
<video prop:srcObject={screen()} autoplay />
<button
onClick={() => setScreenConstraints({ video: true })}
title="We need user interaction to capture screen"
>
{!screenConstraints() ? "Capture Screen" : "Change Window"}
</button>
</div>
</div>
);
};

export default App;
72 changes: 72 additions & 0 deletions packages/mediastream/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"name": "@solid-primitives/mediastream",
"version": "0.1.0",
"description": "Primitives to work with media streams from microphones, cameras, and the screen",
"author": "Alex Lohr <alex.lohr@logmein.com>",
"contributors": [
"David Di Biase <dave@solidjs.com>"
],
"license": "MIT",
"homepage": "https://primitives.solidjs.community/package/mediastream",
"repository": {
"type": "git",
"url": "git+https://github.com/solidjs-community/solid-primitives.git"
},
"bugs": {
"url": "https://github.com/solidjs-community/solid-primitives/issues"
},
"primitive": {
"name": "mediastream",
"stage": 0,
"list": [
"createStream",
"createAmplitudeStream",
"createAmplitudeFromStream",
"createMediaPermissionRequest",
"createScreen"
],
"category": "Browser APIs"
},
"files": [
"dist"
],
"private": false,
"sideEffects": false,
"type": "module",
"module": "./dist/index.js",
"browser": {},
"types": "./dist/index.d.ts",
"exports": {
"import": {
"@solid-primitives/source": "./src/index.ts",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"dev": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/dev.ts",
"build": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/build.ts",
"vitest": "vitest -c ../../configs/vitest.config.ts",
"test": "pnpm run vitest",
"test:ssr": "pnpm run vitest --mode ssr"
},
"keywords": [
"solid",
"mediastream",
"media",
"stream",
"primitives"
],
"dependencies": {
"@solid-primitives/utils": "workspace:^"
},
"peerDependencies": {
"@solidjs/web": "^2.0.0-beta.14",
"solid-js": "^2.0.0-beta.14"
},
"typesVersions": {},
"devDependencies": {
"@solidjs/web": "2.0.0-beta.14",
"solid-js": "2.0.0-beta.14"
}
}
Loading