Skip to main content

Capture, Produce, Consume, and Render Media

This guide follows one media track from permission to cleanup. Use the SDK section that matches your application; helper names are intentionally not copied from one platform to another.

The working sequence

  1. Create or join the room and wait for the SDK's readiness signal.
  2. Ask for the microphone, camera, or display only when the user requests it.
  3. Produce the selected local track through the room runtime.
  4. Consume the participant tracks prepared by the SDK.
  5. Resolve one primary visual: active screen share, applicable remote camera, then local camera.
  6. Mount every prepared remote-audio renderer, even when its participant video is on another page.
  7. Stop app-created tracks and await semantic leave or host end before unmounting.

The supplied room UI performs these stages for you. In a headless room, keep the room runtime mounted and use its headless controller, hook, service, or facade. Do not rebuild socket or mediasoup transport negotiation in the page component.

Read current state without publishing it

getCurrentParams() is the pure read in the TypeScript and Flutter headless layers. getUpdatedAllParams() republishes shared parameters; do not call it from React render, Angular change detection, a Vue computed value or watcher, Flutter build, Compose composition, pagination reads, or a polling timer.

Accept every updateSourceParameters publication. An older parameter bag can become stale, so actions should resolve the latest bag immediately before they run.

Keep remote audio independent of video

Video pagination controls what people see, not who they hear. Keep all prepared remote-audio renderers mounted outside the main-video and mini-video selection. Hiding the audio container visually is fine; unmounting it or coupling it to the visible page is not.

ReactJS 4.3.2

import { AudioGrid, ModernMediasfuGeneric, useMediasfuHeadless } from 'mediasfu-reactjs';

export function RoomMedia() {
const room = useMediasfuHeadless();
const primary = room.screenShare.stream ?? room.remoteVideos[0]?.stream ?? room.localVideo;

return <>
<ModernMediasfuGeneric
returnUI={false}
sourceParameters={room.sourceParameters}
updateSourceParameters={room.updateSourceParameters}
onMediaChanged={room.onMediaChanged} />
<p>{room.ready ? 'Media ready' : room.readiness.reason}</p>
{primary && <video
autoPlay playsInline muted={primary === room.localVideo || room.screenShare.isLocal}
ref={(node) => { if (node && node.srcObject !== primary) node.srcObject = primary; }} />}
<button disabled={!room.ready} onClick={() => void room.controls.toggleMic()}>Microphone</button>
<button disabled={!room.ready} onClick={() => void room.controls.toggleCamera()}>Camera</button>
<button disabled={!room.ready} onClick={() => void room.controls.toggleScreenShare()}>Share screen</button>
<AudioGrid componentsToRender={room.audioComponents} />
</>;
}

Use room.controls.selectMic(deviceId), selectCamera(deviceId), and flipCamera() when the corresponding device is available. Use room.produce for app-created media and stop that media during cleanup.

Angular 2.3.2

Provide one MediasfuHeadlessService for the room screen and bind the SDK's publication callbacks to it.

@Component({
providers: [MediasfuHeadlessService],
template: `
<app-mediasfu-generic
[returnUI]="false"
[sourceParameters]="room.sourceParameters"
[updateSourceParameters]="room.updateSourceParameters"
(mediaChanged)="room.onMediaChanged($event)" />
<button [disabled]="!(room.ready$ | async)" (click)="room.controls.toggleMic()">Microphone</button>
<button [disabled]="!(room.ready$ | async)" (click)="room.controls.toggleCamera()">Camera</button>
<app-audio-grid [componentsToRender]="(room.audioComponents$ | async) ?? []" />
`,
})
export class RoomComponent {
constructor(public readonly room: MediasfuHeadlessService) {}
}

Resolve the primary stream from screenShare$, remoteVideos$, and localVideo$, in that order, then assign it to a video element's srcObject.

Vue 1.1.2

<script setup lang="ts">
import { computed } from 'vue';
import { AudioGrid, ModernMediasfuGeneric, useMediasfuHeadless } from 'mediasfu-vue';
const room = useMediasfuHeadless();
const primary = computed(() =>
room.screenShare.value.stream ?? room.remoteVideos.value[0]?.stream ?? room.localVideo.value
);
</script>

<template>
<ModernMediasfuGeneric
:return-u-i="false"
:source-parameters="room.sourceParameters"
:update-source-parameters="room.updateSourceParameters"
@media-changed="room.onMediaChanged" />
<button :disabled="!room.ready.value" @click="room.controls.toggleMic()">Microphone</button>
<button :disabled="!room.ready.value" @click="room.controls.toggleCamera()">Camera</button>
<AudioGrid :components-to-render="room.audioComponents.value" />
</template>

Bind primary with a component or directive that assigns srcObject; a MediaStream is not a URL.

React Native 2.4.2

const room = useMediasfuHeadless();
const primary = room.screenShare.stream ?? room.remoteVideos[0]?.stream ?? room.localVideo;

return <>
<MediasfuGeneric
returnUI={false}
sourceParameters={room.sourceParameters}
updateSourceParameters={room.updateSourceParameters}
onMediaChanged={room.onMediaChanged} />
{primary && <RTCView streamURL={primary.toURL()} objectFit="cover" style={{ flex: 1 }} />}
<AudioGrid componentsToRender={room.audioComponents} />
</>;

Use contain sizing and no mirroring for a screen. Verify audio routing, Bluetooth, camera switching, permissions, and foreground/background recovery on physical Android and iOS devices.

Expo 2.5.2

Use the React Native pattern from mediasfu-reactnative-expo in an Expo development build. Expo Go does not provide every native WebRTC and screen-capture requirement. Test microphone capture and audible remote playout on two real devices before release.

Flutter 2.3.1

final room = MediasfuHeadlessController();

final runtime = ModernMediasfuGeneric(
options: ModernMediasfuGenericOptions(
returnUI: false,
updateSourceParameters: room.updateSourceParameters,
),
);

// Re-evaluate inside AnimatedBuilder whenever the controller notifies.
final primary = room.screenShare.stream ??
(room.remoteVideos.isNotEmpty ? room.remoteVideos.first.stream : null) ??
room.localVideo;

Render primary with the SDK video component. Use contain/no mirror for screen content and mirror only the local camera preview. Mount all widgets returned by getAudioGridComponents(room.parameters!.getCurrentParams()). Gate controls on room.ready and wrap them with runMediaControl so permission and policy errors reach the user.

Android and Kotlin Multiplatform 1.0.6

The common headless facade provides immutable snapshots and live media handles:

val controller = MediaSfuHeadlessController(engine)
val snapshot = controller.snapshot()

if (snapshot.readiness.mediaControlsReady) {
controller.prepareLocalMedia("video")
}

val participantVideo = controller.participantMedia(
participantId = participantId,
kind = "video"
)

Capture a new snapshot after state changes; an old snapshot is intentionally detached. Android's AndroidWebRtcDevice owns platform capture, device lists, audio output, virtual video, and display capture. Keep those platform actions outside Compose construction and release them from the owning lifecycle.

The snapshot advertises recording, poll, breakout, and whiteboard state, but the current headless controller does not expose those action methods. Use the verified high-level room/UI method layer when you need them.

For a recording that preserves an app-owned layout, follow Record a Custom App-Owned Scene. The scene and its ordered mutations travel through an authenticated application adapter; source binding, pause-aware time, final publication, and completion remain server-owned.

Swift and Apple platforms 0.1.3

The Apple package supplies native transport bridges and re-exports the hosted KMP room framework. It does not currently expose an independent Swift headless room facade. Use MediaSFUIosHostBridge for the hosted room composition and own the SwiftUI/UIKit navigation, permission prompts, audio session, observers, and dismissal cleanup. Do not copy a Kotlin or React headless call into Swift unless the installed Apple package publicly exports that symbol.

Unity 0.1.0-preview.2

var snapshot = headless.CaptureSnapshot();
var participantVideo = headless.ResolveParticipantMedia(participantId, null, "video");
var allRemoteAudio = headless.GetAllAudioTracks();

if (snapshot.Readiness.MediaControlsReady)
{
await headless.SetCameraEnabledAsync(true);
await headless.SetMicrophoneEnabledAsync(true);
}

Bind tracks to scene renderers and audio sources independently. The common Unity contract does not enumerate devices or provide viewer/HLS playback; keep those controls unavailable unless a platform-specific adapter supplies them.

Shared core 1.2.0

mediasfu-shared exposes the framework-neutral building blocks used by the TypeScript SDKs: getCurrentParams, getRoomReadiness, media projections, runMediaControl, moderation/session actions, custom production, playback, WHIP/WHEP, and HLS helpers. It renders no UI and does not prove that a consuming framework binds every shared operation.

Production, consumption, and custom sources

  • Camera and microphone controls produce device tracks through the active room.
  • Screen sharing is a separate source; show the system picker/permission error.
  • Participant media should be resolved by the SDK's participant/producer projection, not by guessed array positions or display names alone.
  • A whiteboard or screenboard is not automatically a display-capture track.
  • App-created canvas, element, file, or synthetic tracks remain app-owned; stop and detach them even if the SDK room also leaves successfully.

Recovery and teardown

Transport acknowledgements can arrive after the user leaves or disables a device. Ignore only a result that belongs to an already closed device, transport, or room; report every other failure. On reconnect, discard superseded sockets/transports and refresh projections before re-enabling media controls.

Before navigation:

  1. stop app-created tracks;
  2. await participant leave, host leave-without-ending, or host end;
  3. dispose subscriptions, listeners, renderers, and audio sources;
  4. clear room-scoped application state;
  5. verify a late socket acknowledgement cannot recreate media.

Continue with headless UI, device controls, leave, end, and rejoin, and large rooms.