--- name: flutter-soloud-streaming version: 3 description: Teaches the push buffer-stream API of the flutter_soloud package (setBufferStream, addAudioDataStream, setDataIsEnded, BufferingType, BufferType, icy metadata). Use when the user wants to play audio that arrives in chunks — internet/icecast radio, WebSocket PCM feeds, TTS/LLM streaming APIs, or procedurally generated PCM — instead of loading a complete file or asset. --- # Push buffer streaming in flutter_soloud Push streaming plays audio while it is still arriving: you create an `AudioSource` with `SoLoud.instance.setBufferStream(...)`, push chunks with `addAudioDataStream(source, bytes)`, and mark the end with `setDataIsEnded(source)`. The engine decodes and buffers internally, auto-pausing when the buffer runs dry and resuming once `bufferingTimeNeeds` seconds are buffered again. This is the API for icecast/web radio, WebSocket audio, streaming TTS, and generated PCM — not for complete files on disk (use `loadFile`/`loadMem`/`loadUrl` for those). ## Minimal example ```dart import 'dart:typed_data'; import 'package:flutter_soloud/flutter_soloud.dart'; Future main() async { await SoLoud.instance.init(); // Synchronous — no await. Returns an AudioSource immediately. final stream = SoLoud.instance.setBufferStream( maxBufferSizeBytes: 1024 * 1024 * 100, // default: 100 MB cap, not pre-allocated bufferingType: BufferingType.released, // free played data (radio-style) bufferingTimeNeeds: 2, // seconds buffered before unpause sampleRate: 44100, channels: Channels.stereo, format: BufferType.auto, // detect MP3, WAV, FLAC, Ogg, AAC, AC-3, E-AC-3 onBuffering: (isBuffering, handle, time) { // isBuffering=true: engine paused, waiting for data // isBuffering=false: resumed }, onMetadata: (metadata) { // AudioMetadata; fires on first data and on mid-stream changes }, ); // Synchronous too. Safe to call before any data arrives — playback // starts once bufferingTimeNeeds seconds are buffered. final handle = SoLoud.instance.play(stream); // ... as chunks arrive (http stream, WebSocket, generator): SoLoud.instance.addAudioDataStream(stream, someUint8ListChunk); // When the feed ends (or never, for live radio): SoLoud.instance.setDataIsEnded(stream); // Optional: know when playback actually finished. Only fires if // setDataIsEnded was called. stream.soundEvents.listen((event) { if (event.event == SoundEventType.handleIsNoMoreValid && event.handle == handle) { // finished playing } }); } ``` ## The API shape All of these live on `SoLoud.instance` and operate on the `AudioSource` returned by `setBufferStream`. Verified against `lib/src/soloud.dart` and `lib/src/enums.dart`: - `AudioSource setBufferStream({int? maxBufferSizeBytes, Duration? maxBufferSizeDuration, BufferingType bufferingType = BufferingType.preserved, double bufferingTimeNeeds = 2, int sampleRate = 24000, Channels channels = Channels.mono, BufferType format = BufferType.s16le, bool autoDispose = false, void Function(bool isBuffering, int handle, double time)? onBuffering, void Function(AudioMetadata)? onMetadata})` — synchronous factory. `maxBufferSizeBytes` and `maxBufferSizeDuration` are mutually exclusive (asserted); neither pre-allocates memory, they only cap how much data may be added. - `void addAudioDataStream(AudioSource source, Uint8List audioChunk)` — push one chunk. Synchronous, callable from any isolate (`@pragma('vm:entry-point')` functions in the example use it). - `void setDataIsEnded(AudioSource sound)` — marks the stream complete. Without this, `soundEvents` never reports playback completion. - `void resetBufferStream(AudioSource sound)` — drops buffered data so you can retune the same source to new content. - `int getBufferSize(AudioSource sound)` — current buffered size in bytes. - `Duration getStreamTimeConsumed(AudioSource sound)` — elapsed play time; **only** for `BufferingType.released` streams (throws for other sounds). - `void setBufferIcyMetaInt(AudioSource sound, int icyMetaInt)` — pass the `icy-metaint` response header of an icecast stream so the engine can strip/extract MP3 or Ogg-FLAC metadata. Must be called once, **before the first** `addAudioDataStream`. `setMp3BufferIcyMetaInt` is a deprecated alias of this. Key enums (`lib/src/enums.dart`): - `BufferType` — `f32le`, `s8`, `s16le`, `s32le` are raw interleaved PCM (you must set `sampleRate`/`channels` correctly). `auto` detects MP3, WAV, FLAC, Ogg (Opus, Vorbis, FLAC), AAC (ADTS), AC-3, and E-AC-3 streams and **ignores** `sampleRate`/`channels` (on Linux, decoding AAC/AC-3/E-AC-3 requires FFmpeg `libavcodec`/`libavformat` runtime libraries; on Web, AC-3 and E-AC-3 streaming is not supported by any browser; core formats work out of the box). - `BufferingType` — `preserved`: keeps all data in memory, allows multiple simultaneous handles, seeking and looping. `released`: frees played data (bounded memory for endless feeds), but plays once, cannot seek, and `getPosition` always returns 0 — use `getStreamTimeConsumed` instead. Divergences from audioplayers/just_audio habits: - No `setUrl`/`AudioSource.uri` streaming; you do the HTTP/WebSocket I/O yourself and push raw bytes. - `setBufferStream` and `play` are **synchronous** — `await SoLoud.instance.play(stream)` compiles but is pointless; the docs snippet showing that is stale. - There is no built-in position stream for `released` streams; poll `getStreamTimeConsumed` on a timer (the example's `buffer_widget.dart` does exactly this). - Buffer underruns are not errors: the engine pauses and fires `onBuffering(isBuffering: true, ...)`, then resumes automatically. ## Traps - **MP4 and M4A containers are NOT supported for streaming.** Container files with ISO BMFF / MP4 atom structures (`moov`, `mdat`) require random access to read header atom offsets and cannot be streamed chunk-by-chunk with `setBufferStream` or `setPullBufferStream`. For real-time streaming, use elementary streams (AAC ADTS, AC-3, E-AC-3, OGG, MP3, WAV, or raw PCM). MP4 and M4A are only supported for full-file/asset/memory loading (`loadFile`, `loadAsset`, `loadMem`, `loadUrl`). - **AC-3 and E-AC-3 streaming is NOT supported on the Web platform in any browser.** Google Chrome, Chromium browsers, and Mozilla Firefox do not support AC-3 or E-AC-3 due to proprietary Dolby Digital licensing restrictions. Safari's WebCodecs engine rejects raw AC-3 elementary stream frames. AC-3 and E-AC-3 streaming is native-only (iOS, macOS native, Android, Windows, and Linux with FFmpeg). On Web, feeding AC-3/E-AC-3 stream data returns `PlayerErrors.audioFormatNotSupported`. For cross-browser web streaming, use AAC (ADTS), Opus, MP3, FLAC, or raw PCM. - **`BufferingType.released` plays exactly once.** A second `play` on the same source throws `SoLoudBufferStreamCanBePlayedOnlyOnceCppException`. When it finishes, the buffer is empty and you must dispose the source yourself (or pass `autoDispose: true`). - **`getPosition` lies for released streams** — always `Duration.zero`. Use `getStreamTimeConsumed(sound)`; calling it on a non-released sound throws. - **Reaching the max buffer size ends the stream.** Once the cap is hit, the stream is treated as ended and further `addAudioDataStream` calls throw `SoLoudStreamEndedAlreadyCppException` (the web_radio example catches it and reconnects). Size the cap for the feed, or use `released`. - **Icy metadata ordering.** Request the header with `Icy-MetaData: 1` on the HTTP request, then call `setBufferIcyMetaInt(source, int.parse(headers['icy-metaint'] ?? '0'))` on the first audio chunk, before any `addAudioDataStream`. Later calls are ignored. - **With `format: BufferType.auto`, `sampleRate` and `channels` are ignored** — the decoder takes them from the container. They only matter for raw PCM formats. - **Raw PCM must match the declared format exactly**: interleaved, little-endian, correct sample rate and channel count; the engine resamples to `sampleRate` but does not channel-convert or fix wrong formats — mismatch means noise or wrong speed, not an error. - **Compressed streaming needs Xiph libs (Ogg, Vorbis, Opus, FLAC).** If the app was built with them excluded (`no_xiph_libs: true`), `addAudioDataStream` of compressed data throws `SoLoudXiphLibsNotAvailableException`. - **Forgetting `setDataIsEnded`** leaves the stream "live" forever: `soundEvents` never emits `handleIsNoMoreValid` and (with `preserved`) the buffer keeps growing. - **`maxBufferSizeDuration` is computed from `sampleRate`/`channels`** as `ms * sampleRate * channels * 4 bytes` (internal float storage) — for `auto` sources this conversion is approximate. ## More depth - Full recipes (icecast radio with icy metadata, WebSocket PCM, procedural PCM in an isolate): [references/recipes.md](references/recipes.md) - Pull-model streaming (engine requests data via callback; for huge files and byte-range fetching) is covered by the sibling skill **flutter-soloud-pull-streaming** (`skills/flutter-soloud-pull-streaming/SKILL.md`). - Runnable demos in this repo: `example/lib/buffer_stream/web_radio.dart`, `websocket.dart`, `generate.dart`, `simple_noise_stream.dart` (the last one is the smallest complete example). ## Keeping this skill current This skill ships inside the flutter_soloud package, so upgrading flutter_soloud can carry a newer revision of it than the copy installed in the project. To check, run: ```sh dart run flutter_soloud:skills --check ``` It reports the installed and bundled skill versions and exits non-zero when an update is available. Offer to update with `dart run flutter_soloud:skills` (which touches only the skills, never pubspec or build files).