Skip to content

feat(plugin): audio stream#4396

Open
ArjixWasTaken wants to merge 50 commits into
masterfrom
plugin/audio-stream
Open

feat(plugin): audio stream#4396
ArjixWasTaken wants to merge 50 commits into
masterfrom
plugin/audio-stream

Conversation

@ArjixWasTaken

@ArjixWasTaken ArjixWasTaken commented Mar 30, 2026

Copy link
Copy Markdown
Member

Audio Stream Plugin

Description

This plugin streams Pear Desktop's audio over HTTP as Opus in an Ogg container (Icecast-compatible). Any player or device on your local network — mpv, VLC, browsers, or other Icecast clients — can connect to a single URL and listen in real time, with per-song metadata embedded in the stream.

How It Works

  1. Renderer Process: Taps the app's Web Audio graph with an AudioWorkletProcessor, bridges it to a 48 kHz AudioContext (so the rate is always Opus-valid), and encodes the audio to Opus in real time using the native WebCodecs AudioEncoder. The resulting Opus packets (and the OpusHead) are sent to the backend over IPC.
  2. Backend Process: A small hand-written Ogg muxer wraps the incoming Opus packets into a live Ogg/Opus stream served over HTTP (/stream). Per-song metadata (title/artist/album) is carried in-band via chained Ogg logical streams: when the track changes, the current logical stream ends and a new one begins with fresh OpusTags, while the granule position stays monotonic so playback clocks never jump backward. Header pages are cached so clients that join mid-stream initialise cleanly.
  3. Client adaptation: Some clients can't follow Ogg chains — browser /MSE reload on a new logical stream, and VLC's clock chokes on it. Those clients (detected by User-Agent) are transparently served a de-chained single logical stream instead, so they play continuously across song changes (without the in-band title updates). mpv and other chain-capable players get the full chained stream with live "now playing" metadata.
  4. Configuration: Streaming port, hostname, bitrate, channels, and buffer size are configurable through the plugin menu.

Key Features

  • Opus/Ogg streaming over HTTP at a consistent bitrate (default 128 kbps)
  • In-band per-song metadata (title/artist/album) via chained Ogg — updates live
  • Automatic client adaptation — browsers and VLC get a continuous single-stream variant; no reload or rebuffering on song change
  • ICY headers (icy-name, icy-url, icy-audio-info, …) for Icecast-aware clients
  • Multiple simultaneous clients, with header-page caching for late joiners
  • Dynamic configuration — settings change without restarting the app

Use Cases

  • Listening to Pear Desktop from any device on the LAN via a standard media pla
  • Re-broadcasting / integrating with an Icecast setup
  • Recording or processing the audio with external software
  • Audio routing to other applications or devices

Technical Details

  • Default port: 8765
  • Default hostname: 0.0.0.0 (all interfaces)
  • Audio: Opus, 48 kHz, stereo, 128 kbps (default)
  • Encode: WebCodecs AudioEncoder in the renderer; mux: custom Ogg muxer in the
  • Endpoint: GET /stream (Content-Type: audio/ogg)
  • Chained Ogg for chain-capable clients; de-chained single logical stream for b

Configuration

Configurable through the Pear Desktop menu:

  • Port: HTTP server port (default: 8765)
  • Quality & Latency: sample rate, bitrate, channels, and buffer size

Stream URL format: http://localhost:{port}/stream

Summary by CodeRabbit

  • New Features
    • Added an audio streaming plugin with a new settings menu for stream port and “Quality & latency” (sample rate, bitrate, channels, buffer size).
    • Streams browser audio to a local backend as Opus, with automatic stream metadata/headers and a live stream URL.
    • Improves compatibility by delivering Ogg/Opus as chained streams when supported, and seamlessly de-chaining for other clients.
  • Bug Fixes
    • Improves reliability on slow connections by dropping lagging subscribers instead of allowing unbounded buffering.

dnecra and others added 30 commits December 18, 2025 01:54
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@ArjixWasTaken ArjixWasTaken added the enhancement New feature or request label Mar 30, 2026
@ArjixWasTaken

This comment was marked as resolved.

@pear-devs pear-devs deleted a comment from coderabbitai Bot Jun 17, 2026
@coderabbitai

This comment was marked as spam.

@ArjixWasTaken ArjixWasTaken marked this pull request as ready for review June 17, 2026 10:39
@ArjixWasTaken ArjixWasTaken changed the title new plugin: audio-stream feat(plugin): audio stream Jun 17, 2026
@ArjixWasTaken

ArjixWasTaken commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

This comment was marked as spam.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ArjixWasTaken ArjixWasTaken requested a review from JellyBrick June 17, 2026 10:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/plugins/audio-stream/backend.ts`:
- Around line 40-84: The GET '/stream' route handler in the Hono app definition
has multiple Prettier formatting violations. Run Prettier on this code block to
automatically fix the formatting issues. This will ensure the code meets the
project's ESLint and Prettier formatting standards for the route handler,
including proper indentation, line lengths, and spacing throughout the handler
definition.
- Around line 126-133: The `stop()` method in the audio-stream backend creates a
Promise that only resolves when the server's close callback is invoked, but if
`this.server` is undefined, the callback never fires and the Promise hangs
indefinitely. Modify the `stop()` method to check if `this.server` exists before
calling `close()` on it; if the server is undefined, resolve the Promise
immediately without waiting for the callback, otherwise proceed with the
existing logic of passing the resolve callback to the `close()` method.
- Around line 41-50: The `/stream` endpoint route handler is missing CORS
headers required for cross-origin browser access. Add the
`Access-Control-Allow-Origin` header to the response in the GET `/stream` route
handler (where ctx.header calls are made for Content-Type and
Transfer-Encoding). Set the header to allow cross-origin requests from browsers,
typically using a value like '*' for public access or a specific origin if
needed. This should be added alongside the existing ctx.header calls that set
Content-Type and Transfer-Encoding.
- Around line 56-59: The `icy-audio-info` header value in the audio stream
backend is hardcoding `ice-channels=2` which incorrectly reports stereo
regardless of the actual channel configuration. Replace the hardcoded value `2`
in the `ice-channels=2` portion of the template literal with the configured
channel count from the config object (likely `config.channels`), so the header
accurately reflects whether mono or stereo is selected.

In `@src/plugins/audio-stream/BroadcastStream.ts`:
- Around line 26-33: The write method in BroadcastStream unconditionally
enqueues chunks to all subscribers, which can cause memory buildup when slow
clients fall behind. Before calling controller.enqueue(chunk) in the write
method, check the controller's desiredSize property. If desiredSize is less than
or equal to zero, indicating a backed-up queue, remove the controller from the
subscribers set instead of enqueueing. Only enqueue the chunk to controllers
with desiredSize greater than zero to prevent unbounded buffering on slow
clients.

In `@src/plugins/audio-stream/menu.ts`:
- Around line 1-12: Fix the import order and prettier formatting violations in
the file by organizing imports following the project's convention: external
dependencies first, then internal imports from `@/` paths, then type imports, with
each group sorted alphabetically. Run prettier on the file to auto-correct
formatting issues. The violations are reported at multiple locations throughout
the file (lines 1-12, 65-66, 80-81, 95-98, 110-111), so ensure all import
statements are properly reorganized and formatted consistently to pass static
analysis checks.
- Around line 55-57: The setConfig calls are spreading the entire config object
and then overriding the changed field, which can leak unwanted fields like
enabled across the config boundary. Instead of using the spread operator pattern
with config or currentConfig, pass only the specific field being updated to
setConfig. This pattern needs to be fixed at all five locations in the menu.ts
file where setConfig is called: the port update around line 56, the
currentConfig port update around line 74, the currentConfig name update around
line 89, the currentConfig volume update around line 104, and the currentConfig
pitch update around line 119. Replace each instance with a call to setConfig
that includes only the changed key-value pair.

In `@src/plugins/audio-stream/ogg-opus.ts`:
- Line 121: The return statement containing the this.rewrite method call with
bitwise operations has a Prettier formatting violation. Run Prettier's
auto-formatter on the file or manually reformat the return statement with the
bitwise AND and OR operations to comply with Prettier's line length and
formatting rules for the expression involving HEADER_TYPE_BOS and
HEADER_TYPE_EOS constants.

In `@src/plugins/audio-stream/renderer.ts`:
- Around line 174-182: The isStreaming flag is being set to true twice: once
inside the .then() block after successful worklet initialization (correct
placement) and once synchronously immediately after the .catch() handler before
the async operation completes. Remove the synchronous this.isStreaming = true
assignment that appears after the .catch() block to ensure the flag only becomes
true after the addModule promise successfully resolves, preventing other code
from thinking streaming is active when the worklet setup hasn't actually
completed yet.
- Around line 76-81: The startStreaming method accepts a bufferSize parameter
but no call sites pass this.config.bufferSize to it, so configuration changes to
buffer size have no effect. Find all invocations of the startStreaming method
throughout the file and add this.config.bufferSize as the bufferSize argument.
Additionally, update the restart logic within the onConfigChange method (around
line 271) to also pass this.config.bufferSize when restarting the stream,
ensuring that buffer size changes from the menu are properly applied.

In `@src/plugins/audio-stream/StreamProcessor.js`:
- Around line 27-30: The output passthrough logic in the condition block
checking outputs[0] and inputs[0] assumes that if outputs[0] exists as an array,
its channel indices will be accessible. However, outputs[0] can be an empty
array with no channels, causing outputs[0][0] and outputs[0][1] to be undefined.
Before calling .set() on outputs[0][0], add a check to verify that this channel
actually exists in the array. Similarly, the existing check for inputs[0][1]
should have a corresponding check to verify outputs[0][1] exists before calling
.set() on it. Add these existence checks for both output channels to prevent
TypeError crashes when the worklet attempts to call .set() on undefined values.

In `@src/plugins/audio-stream/test-gui.html`:
- Around line 469-510: The entire parsing logic in the while loop starting at
line 469 expects a custom wire format with JSON metadata followed by PCM data,
but the backend now sends Ogg/Opus encoded audio. Remove the JSON metadata
parsing logic (the metadata length reading, TextDecoder, and JSON.parse
sections) and either replace the stream processing to use the browser's native
<audio> element with the Ogg stream URL directly, or implement Ogg page parsing
and WebCodecs AudioDecoder to decode the Opus audio. The audioConfig
initialization based on parsed metadata will need to be restructured
accordingly.
- Around line 779-805: The reader variable obtained from
response.body.getReader() inside the connect() function is scoped locally and
cannot be cancelled from disconnect(), causing the fetch read loop to continue
running even after disconnect is called and potentially causing errors when it
accesses nullified audioContext or analyserNode. Move the reader variable
declaration to module scope so it can be accessed from both connect() and
disconnect() functions, then call reader.cancel() in the disconnect() function
before nullifying the audio-related variables to properly stop the read loop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0cdb39b3-4947-46bb-be49-b8c730bb157c

📥 Commits

Reviewing files that changed from the base of the PR and between 3669208 and ef89712.

📒 Files selected for processing (11)
  • src/i18n/resources/en.json
  • src/pear-desktop.ts
  • src/plugins/audio-stream/BroadcastStream.ts
  • src/plugins/audio-stream/StreamProcessor.js
  • src/plugins/audio-stream/backend.ts
  • src/plugins/audio-stream/config.ts
  • src/plugins/audio-stream/index.ts
  • src/plugins/audio-stream/menu.ts
  • src/plugins/audio-stream/ogg-opus.ts
  • src/plugins/audio-stream/renderer.ts
  • src/plugins/audio-stream/test-gui.html

Comment thread src/plugins/audio-stream/backend.ts Outdated
Comment thread src/plugins/audio-stream/backend.ts Outdated
Comment thread src/plugins/audio-stream/backend.ts Outdated
Comment thread src/plugins/audio-stream/backend.ts
Comment thread src/plugins/audio-stream/BroadcastStream.ts
Comment thread src/plugins/audio-stream/renderer.ts
Comment thread src/plugins/audio-stream/renderer.ts
Comment thread src/plugins/audio-stream/StreamProcessor.js
Comment thread src/plugins/audio-stream/test-gui.html Outdated
Comment thread src/plugins/audio-stream/test-gui.html Outdated
@ArjixWasTaken

Copy link
Copy Markdown
Member Author

Huh, coderabbit did a nice review

@ArjixWasTaken ArjixWasTaken self-assigned this Jun 17, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit

oxlint

🚫 [oxlint] <typescript(no-unsafe-member-access)> reported by reviewdog 🐶
Unsafe member access [0] on an any value.

if (outputs[0][1] && inputs[0][1]) outputs[0][1].set(inputs[0][1]);


🚫 [oxlint] <typescript(no-unsafe-member-access)> reported by reviewdog 🐶
Unsafe member access [0] on an any value.

if (outputs[0][1] && inputs[0][1]) outputs[0][1].set(inputs[0][1]);


🚫 [oxlint] <typescript(no-unsafe-call)> reported by reviewdog 🐶
Unsafe call of a(n) error type typed value.

registerProcessor('recorder-processor', RecorderProcessor);


🚫 [oxlint] <perfectionist(sort-imports)> reported by reviewdog 🐶
Expected "./backend" to come before "./config".

import { backend } from './backend';


🚫 [oxlint] <perfectionist(sort-imports)> reported by reviewdog 🐶
Expected "./config" to come before "@/types/music-player".

import type { AudioStreamConfig } from './config';


🚫 [oxlint] <perfectionist(sort-imports)> reported by reviewdog 🐶
Extra spacing between "@/types/music-player" and "./config".

import type { AudioStreamConfig } from './config';


🚫 [oxlint] <typescript(no-unsafe-assignment)> reported by reviewdog 🐶
Unsafe assignment of an any value.

const interleaved: Float32Array = event.data;


🚫 [oxlint] <perfectionist(sort-imports)> reported by reviewdog 🐶
Expected "@hono/node-server" to come before "hono/streaming".

import { serve, type ServerType } from '@hono/node-server';


🚫 [oxlint] <perfectionist(sort-imports)> reported by reviewdog 🐶
Expected "@/providers/song-info" to come before "@/utils".

import { registerCallback, type SongInfo } from '@/providers/song-info';


🚫 [oxlint] <perfectionist(sort-imports)> reported by reviewdog 🐶
Missed spacing between "@/providers/song-info" and "./config".

import { type AudioStreamConfig } from './config';


🚫 [oxlint] <perfectionist(sort-imports)> reported by reviewdog 🐶
Expected "./BroadcastStream" to come before "./config".

import { BroadcastStream } from './BroadcastStream';

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Build Artifacts Ready!

The builds have completed successfully. You can download the artifacts from the workflow run:

📦 Download Artifacts

Available builds:

  • Windows: build-artifacts-windows-latest
  • macOS: build-artifacts-macos-latest
  • Linux: build-artifacts-ubuntu-latest

Note: Artifacts are available for 7 days.

@ArjixWasTaken

ArjixWasTaken commented Jun 28, 2026

Copy link
Copy Markdown
Member Author

@JellyBrick CodeRabbit is now happy, oxlint doesn't complain, only macos build fails for whatever reason

maybe this could make it into the 3.12 release?

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Build Artifacts Ready!

The builds have completed successfully. You can download the artifacts from the workflow run:

📦 Download Artifacts

Available builds:

  • Windows: build-artifacts-windows-latest
  • macOS: build-artifacts-macos-latest
  • Linux: build-artifacts-ubuntu-latest

Note: Artifacts are available for 7 days.

@github-actions

Copy link
Copy Markdown
Contributor

❌ Build Failed

Unfortunately, one or more builds failed. Please check the workflow run for details:

View Workflow Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants