Custom sources in React Native Mobile
This guide is exclusively for Mobile (React Native) applications.
@fishjam-cloud/react-native-client provides two hooks for publishing custom sources. Your app must be wrapped in FishjamProvider.
useCustomSourcepublishes aMediaStream. It is the same hook that is available on the web, typed for the React NativeMediaStream. The stream typically comes from the Vision Camera integration or the low-level frame API, whose hooks create it for you, but anyMediaStreamyou hold works.useCustomAudioSourcepublishes audio your app generates itself: you push PCM samples and the hook manages the track and its stream. See Publish custom audio.
Publish a stream
Set the stream on a stable source ID; set null to unpublish. The cleanup function keeps the source in sync with the component lifecycle.
export functionusePublishStream (stream :MediaStream | null) { const {setStream } =useCustomSource ("my-custom-source");useEffect (() => { if (!stream ) return;setStream (stream ); return () => {setStream (null); }; }, [stream ,setStream ]); }
Tracks are sent to Fishjam only while the peer is connected to a room. A stream set before joining is queued and published as soon as the connection is established, and pending sources are re-published automatically after a reconnect. Calls to setStream are serialized internally, so replacing one stream with another in quick succession is safe.
Custom sources are shared by ID: every useCustomSource call with the same source ID returns the same state, so multiple components can control and read one source.
Show a self-view
The stream returned by useCustomSource renders like any other stream:
export functionSelfView () { const {stream } =useCustomSource ("my-custom-source"); if (!stream ) return null; return ( <RTCView mediaStream ={stream }style ={{height : 200,width : 200 }}objectFit ="cover" /> ); }
Remove the source
const {setStream } =useCustomSource ("my-custom-source"); // ... awaitsetStream (null);
Removing a source unpublishes its tracks but does not stop them; the producer of the stream owns the tracks and should stop them itself.
Publish custom audio
React Native has no equivalent of the web's captureStream() or Web Audio, so audio your app generates does not arrive as a MediaStream. The useCustomAudioSource hook covers this case: it creates a custom audio track, publishes it under its source ID, and cleans it up. Your code pushes raw PCM samples with pushAudioSamples whenever your source produces them, for example a synthesizer, a decoder, or react-native-audio-api:
const {startStreaming ,stopStreaming ,track } =useCustomAudioSource ({sampleRateHz : 48000,channelCount : 1, }); awaitstartStreaming (); // Whenever your source produces audio:onAudioChunk ((chunk ) => { if (track )pushAudioSamples (track ,chunk ); }); // When done: awaitstopStreaming ();
The published track behaves like a live microphone: pushes are re-paced into a continuous stream, and pauses in pushing play as silence. The device microphone is not involved; publishing does not trigger the recording permission, and microphone capture stays with useMicrophone.
Notes on pushing samples:
- Push any chunk size; the native layer re-frames and paces it.
Float32Arraysamples are expected in[-1, 1];Int16Arrayis taken as-is. Stereo tracks take interleaved samples. - The
trackhandle is plain and worklet-serializable, so samples can be pushed straight from a worklet without hopping through the JS thread. - Pushed-but-not-yet-sent audio is buffered up to
maxBufferedDurationMsand drains in real time, so a long clip (for example a text-to-speech utterance) can be handed over in one call.
Options
| Option | Default | Description |
|---|---|---|
sourceId | "customAudioSource" | Source ID the track is published under. Give each instance that streams at the same time its own ID. |
sampleRateHz | 48000 | Sample rate of the PCM you push; a positive multiple of 100. Any rate is resampled downstream. |
channelCount | 1 | 1 for mono, 2 for interleaved stereo. |
maxBufferedDurationMs | 60000 | How much pushed-but-not-yet-sent audio to hold, in milliseconds, before the oldest is dropped. |
Custom audio tracks require the New Architecture. On the old architecture startStreaming does not throw; it fails internally and the failure surfaces in the hook's error field.
Receiving on other peers
Custom tracks arrive in the customVideoTracks and customAudioTracks arrays of each peer returned by usePeers. See Receiving custom tracks.
Related guides
- Vision Camera: publish a camera feed with frame processors
- WebGPU effects: draw your own content into the published video
- Low-level frame API: publish frames from your own pipeline
- How custom sources work