---
title: Local Participant
description: Manage local user media devices, audio, video, and screenshare in RealtimeKit meetings.
image: https://edgetunnel-b2h.pages.dev/dev-products-preview.png
---

> Documentation Index  
> Fetch the complete documentation index at: https://edgetunnel-b2h.pages.dev/realtime/llms.txt  
> Use this file to discover all available pages before exploring further. 

[Skip to content](#%5Ftop) 

# Local Participant

Manage local user media devices, control audio, video, and screenshare, and handle events in RealtimeKit meetings.

Prerequisites

Initialize the SDK and understand the meeting object structure. Refer to [Initialize SDK](https://edgetunnel-b2h.pages.dev/realtime/realtimekit/core/) and [Meeting Object Explained](https://edgetunnel-b2h.pages.dev/realtime/realtimekit/core/meeting-object-explained/).

## Introduction

The local user is accessible via `meeting.self` and contains all information and methods related to the current participant. This includes media controls, device management, participant metadata, and state information.

## Properties

WebMobile

ReactWeb ComponentsAngular

### Metadata Properties

Access participant identifiers and display information:

**JavaScript**

```js
// Participant identifiers
meeting.self.id; // Peer ID (unique per session)
meeting.self.userId; // User ID (persistent across sessions)
meeting.self.customParticipantId; // Custom identifier set by developer
meeting.self.name; // Display name
meeting.self.picture; // Display picture URL
```

```jsx
import { useRealtimeKitSelector } from "@cloudflare/realtimekit-react";


// Participant identifiers
const id = useRealtimeKitSelector((m) => m.self.id);
const userId = useRealtimeKitSelector((m) => m.self.userId);
const customParticipantId = useRealtimeKitSelector(
  (m) => m.self.customParticipantId,
);
const name = useRealtimeKitSelector((m) => m.self.name);
const picture = useRealtimeKitSelector((m) => m.self.picture);
```

**Kotlin**

```kotlin
// Participant identifiers
meeting.localUser.id // Peer ID (unique per session)
meeting.localUser.userId // User ID (persistent across sessions)
meeting.localUser.customParticipantId // Custom identifier set by developer
meeting.localUser.name // Display name
meeting.localUser.picture // Display picture URL
```

**Swift**

```swift
// Participant identifiers
meeting.localUser.id // Peer ID (unique per session)
meeting.localUser.userId // User ID (persistent across sessions)
meeting.localUser.customParticipantId // Custom identifier set by developer
meeting.localUser.name // Display name
meeting.localUser.picture // Display picture URL
```

```jsx
import { useRealtimeKitSelector } from "@cloudflare/realtimekit-react-native";


// Participant identifiers
const id = useRealtimeKitSelector((m) => m.self.id);
const userId = useRealtimeKitSelector((m) => m.self.userId);
const customParticipantId = useRealtimeKitSelector(
  (m) => m.self.customParticipantId,
);
const name = useRealtimeKitSelector((m) => m.self.name);
const picture = useRealtimeKitSelector((m) => m.self.picture);
```

**Dart**

```dart
// Participant identifiers
meeting.localUser.id // Peer ID (unique per session)
meeting.localUser.userId // User ID (persistent across sessions)
meeting.localUser.customParticipantId // Custom identifier set by developer
meeting.localUser.name // Display name
meeting.localUser.picture // Display picture URL
```

### Media Properties

Access the local user's media tracks and states:

**JavaScript**

```js
// Media state flags
meeting.self.audioEnabled; // Boolean: Is audio enabled?
meeting.self.videoEnabled; // Boolean: Is video enabled?
meeting.self.screenShareEnabled; // Boolean: Is screen share active?


// Media tracks (MediaStreamTrack objects)
meeting.self.audioTrack; // Audio MediaStreamTrack (available when audioEnabled is true)
meeting.self.videoTrack; // Video MediaStreamTrack (available when videoEnabled is true)
meeting.self.screenShareTracks; // Object: { video: MediaStreamTrack, audio?: MediaStreamTrack }


// Permissions granted by user
meeting.self.mediaPermissions; // Current audio/video permissions
```

```jsx
// Media state flags
const audioEnabled = useRealtimeKitSelector((m) => m.self.audioEnabled);
const videoEnabled = useRealtimeKitSelector((m) => m.self.videoEnabled);
const screenShareEnabled = useRealtimeKitSelector(
  (m) => m.self.screenShareEnabled,
);


// Media tracks (MediaStreamTrack objects)
const audioTrack = useRealtimeKitSelector((m) => m.self.audioTrack);
const videoTrack = useRealtimeKitSelector((m) => m.self.videoTrack);
const screenShareTracks = useRealtimeKitSelector(
  (m) => m.self.screenShareTracks,
);


// Permissions granted by user
const mediaPermissions = useRealtimeKitSelector((m) => m.self.mediaPermissions);
```

**Kotlin**

```kotlin
// Media state flags
meeting.localUser.audioEnabled // Boolean: Is audio enabled?
meeting.localUser.videoEnabled // Boolean: Is video enabled?
meeting.localUser.screenShareEnabled // Boolean: Is screen share active?


// Permissions granted by user
meeting.localUser.isCameraPermissionGranted // Camera permission status
meeting.localUser.isMicrophonePermissionGranted // Microphone permission status
```

**Swift**

```swift
// Media state flags
meeting.localUser.audioEnabled // Boolean: Is audio enabled?
meeting.localUser.videoEnabled // Boolean: Is video enabled?
meeting.localUser.screenShareEnabled // Boolean: Is screen share active?


// Permissions granted by user
meeting.localUser.isCameraPermissionGranted // Camera permission status
meeting.localUser.isMicrophonePermissionGranted // Microphone permission status
```

```jsx
// Media state flags
const audioEnabled = useRealtimeKitSelector((m) => m.self.audioEnabled);
const videoEnabled = useRealtimeKitSelector((m) => m.self.videoEnabled);
const screenShareEnabled = useRealtimeKitSelector(
  (m) => m.self.screenShareEnabled,
);


// Media tracks (MediaStreamTrack objects)
const audioTrack = useRealtimeKitSelector((m) => m.self.audioTrack);
const videoTrack = useRealtimeKitSelector((m) => m.self.videoTrack);
const screenShareTracks = useRealtimeKitSelector(
  (m) => m.self.screenShareTracks,
);


// Permissions granted by user
const mediaPermissions = useRealtimeKitSelector((m) => m.self.mediaPermissions);
```

**Dart**

```dart
// Media state flags
meeting.localUser.audioEnabled // Boolean: Is audio enabled?
meeting.localUser.videoEnabled // Boolean: Is video enabled?
meeting.localUser.screenShareEnabled // Boolean: Is screen share active?


// Permissions granted by user
meeting.localUser.isCameraPermissionGranted // Camera permission status
meeting.localUser.isMicrophonePermissionGranted // Microphone permission status
```

### State Properties

Access room state and participant status:

**JavaScript**

```js
// Room state
meeting.self.roomJoined; // Boolean: Has joined the meeting?
meeting.self.roomState; // Current room state (see possible values below)
meeting.self.isPinned; // Boolean: Is the local user pinned?


// Permissions and config
meeting.self.permissions; // Capabilities defined by preset
meeting.self.config; // Configuration for meeting appearance
```

**Room state values:**

* `'init'` \- Initialized but not joined
* `'joined'` \- Successfully joined the meeting
* `'waitlisted'` \- Waiting in the waiting room
* `'rejected'` \- Entry rejected
* `'kicked'` \- Removed from meeting
* `'left'` \- Left the meeting
* `'ended'` \- Meeting has ended
* `'disconnected'` \- Disconnected from meeting

```jsx
// Room state
const roomJoined = useRealtimeKitSelector((m) => m.self.roomJoined);
const roomState = useRealtimeKitSelector((m) => m.self.roomState);
const isPinned = useRealtimeKitSelector((m) => m.self.isPinned);


// Permissions and config
const permissions = useRealtimeKitSelector((m) => m.self.permissions);
const config = useRealtimeKitSelector((m) => m.self.config);
```

**Example: Conditional rendering based on room state**

```jsx
const roomState = useRealtimeKitSelector((m) => m.self.roomState);


return (
  <>
    {roomState === "disconnected" && <div>You are disconnected</div>}
    {roomState === "waitlisted" && <div>Waiting for host to admit you</div>}
    {roomState === "joined" && <div>You are in the meeting</div>}
  </>
);
```

**Room state values:**

* `'init'` \- Initialized but not joined
* `'joined'` \- Successfully joined the meeting
* `'waitlisted'` \- Waiting in the waiting room
* `'rejected'` \- Entry rejected
* `'kicked'` \- Removed from meeting
* `'left'` \- Left the meeting
* `'ended'` \- Meeting has ended
* `'disconnected'` \- Disconnected from meeting

**Kotlin**

```kotlin
// Room state
meeting.localUser.roomJoined // Boolean: Has joined the meeting?
meeting.localUser.waitListStatus // Waitlist status (None, Waiting, Accepted, Rejected)
meeting.localUser.isPinned // Boolean: Is the local user pinned?


// Permissions and config
meeting.localUser.permissions // Capabilities defined by preset
meeting.localUser.presetName // Name of preset for local user
meeting.localUser.presetInfo // Typed object representing preset information
```

**Swift**

```swift
// Room state
meeting.localUser.roomJoined // Boolean: Has joined the meeting?
meeting.localUser.waitListStatus // Waitlist status (None, Waiting, Accepted, Rejected)
meeting.localUser.isPinned // Boolean: Is the local user pinned?


// Permissions and config
meeting.localUser.permissions // Capabilities defined by preset
meeting.localUser.presetName // Name of preset for local user
meeting.localUser.presetInfo // Typed object representing preset information
```

```jsx
// Room state
const roomJoined = useRealtimeKitSelector((m) => m.self.roomJoined);
const roomState = useRealtimeKitSelector((m) => m.self.roomState);
const isPinned = useRealtimeKitSelector((m) => m.self.isPinned);


// Permissions and config
const permissions = useRealtimeKitSelector((m) => m.self.permissions);
const config = useRealtimeKitSelector((m) => m.self.config);
```

**Example: Conditional rendering based on room state**

```jsx
const roomState = useRealtimeKitSelector((m) => m.self.roomState);


return (
  <>
    {roomState === "disconnected" && <Text>You are disconnected</Text>}
    {roomState === "waitlisted" && <Text>Waiting for host to admit you</Text>}
    {roomState === "joined" && <Text>You are in the meeting</Text>}
  </>
);
```

**Room state values:**

* `'init'` \- Initialized but not joined
* `'joined'` \- Successfully joined the meeting
* `'waitlisted'` \- Waiting in the waiting room
* `'rejected'` \- Entry rejected
* `'kicked'` \- Removed from meeting
* `'left'` \- Left the meeting
* `'ended'` \- Meeting has ended
* `'disconnected'` \- Disconnected from meeting

**Dart**

```dart
// Room state
meeting.localUser.isHost // Boolean: Is the local user a host?
meeting.localUser.isPinned // Boolean: Is the local user pinned?
meeting.localUser.stageStatus // Stage status of the local user


// Permissions and flags
meeting.localUser.flags // ParticipantFlags (recorder, hidden)
```

## Media Controls

### Audio control

Mute and unmute the microphone:

**JavaScript**

```js
// Enable audio (unmute)
await meeting.self.enableAudio();


// Disable audio (mute)
await meeting.self.disableAudio();


// Check current status
const isAudioEnabled = meeting.self.audioEnabled;
```

```jsx
import { useRealtimeKitClient } from "@cloudflare/realtimekit-react";


function AudioControls() {
  const [meeting] = useRealtimeKitClient();
  const audioEnabled = useRealtimeKitSelector((m) => m.self.audioEnabled);


  const toggleAudio = async () => {
    if (audioEnabled) {
      await meeting.self.disableAudio();
    } else {
      await meeting.self.enableAudio();
    }
  };


  return (
    <button onClick={toggleAudio}>{audioEnabled ? "Mute" : "Unmute"}</button>
  );
}
```

**Kotlin**

```kotlin
// Enable audio (unmute)
meeting.localUser.enableAudio { error: AudioError? -> }


// Disable audio (mute)
meeting.localUser.disableAudio { error: AudioError? -> }


// Check current status
val isAudioEnabled = meeting.localUser.audioEnabled
```

**Swift**

```swift
// Enable audio (unmute)
meeting.localUser.enableAudio { err in }


// Disable audio (mute)
meeting.localUser.disableAudio { err in }


// Check current status
let isAudioEnabled = meeting.localUser.audioEnabled
```

```jsx
import {
  useRealtimeKitClient,
  useRealtimeKitSelector,
} from "@cloudflare/realtimekit-react-native";
import { TouchableHighlight, Text } from "react-native";


function AudioControls() {
  const [meeting] = useRealtimeKitClient();
  const audioEnabled = useRealtimeKitSelector((m) => m.self.audioEnabled);


  const toggleAudio = async () => {
    if (audioEnabled) {
      await meeting.self.disableAudio();
    } else {
      await meeting.self.enableAudio();
    }
  };


  return (
    <TouchableHighlight onPress={toggleAudio}>
      <Text>{audioEnabled ? "Mute" : "Unmute"}</Text>
    </TouchableHighlight>
  );
}
```

**Dart**

```dart
// Enable audio (unmute)
meeting.localUser.enableAudio(onResult: (e) {
  // handle error if any
});


// Disable audio (mute)
meeting.localUser.disableAudio(onResult: (e) {
  // handle error if any
});


// Check current status
final isAudioEnabled = meeting.localUser.audioEnabled;
```

### Video control

Enable and disable the camera:

**JavaScript**

```js
// Enable video
await meeting.self.enableVideo();


// Disable video
await meeting.self.disableVideo();


// Check current status
const isVideoEnabled = meeting.self.videoEnabled;
```

```jsx
function VideoControls() {
  const [meeting] = useRealtimeKitClient();
  const videoEnabled = useRealtimeKitSelector((m) => m.self.videoEnabled);


  const toggleVideo = async () => {
    if (videoEnabled) {
      await meeting.self.disableVideo();
    } else {
      await meeting.self.enableVideo();
    }
  };


  return (
    <button onClick={toggleVideo}>
      {videoEnabled ? "Stop Video" : "Start Video"}
    </button>
  );
}
```

**Kotlin**

```kotlin
// Enable video
meeting.localUser.enableVideo { error: VideoError? -> }


// Disable video
meeting.localUser.disableVideo { error: VideoError? -> }


// Check current status
val isVideoEnabled = meeting.localUser.videoEnabled
```

**Swift**

```swift
// Enable video
meeting.localUser.enableVideo { err in }


// Disable video
meeting.localUser.disableVideo { err in }


// Check current status
let isVideoEnabled = meeting.localUser.videoEnabled
```

```jsx
function VideoControls() {
  const [meeting] = useRealtimeKitClient();
  const videoEnabled = useRealtimeKitSelector((m) => m.self.videoEnabled);


  const toggleVideo = async () => {
    if (videoEnabled) {
      await meeting.self.disableVideo();
    } else {
      await meeting.self.enableVideo();
    }
  };


  return (
    <TouchableHighlight onPress={toggleVideo}>
      <Text>{videoEnabled ? "Stop Video" : "Start Video"}</Text>
    </TouchableHighlight>
  );
}
```

**Dart**

```dart
// Enable video
meeting.localUser.enableVideo(onResult: (e) {
  // handle error if any
});


// Disable video
meeting.localUser.disableVideo(onResult: (e) {
  // handle error if any
});


// Check current status
final isVideoEnabled = meeting.localUser.videoEnabled;
```

### Screen share control

Start and stop screen sharing:

**JavaScript**

```js
// Enable screen share
await meeting.self.enableScreenShare();


// Disable screen share
await meeting.self.disableScreenShare();


// Check current status
const isScreenShareEnabled = meeting.self.screenShareEnabled;
```

```jsx
function ScreenShareControls() {
  const [meeting] = useRealtimeKitClient();
  const screenShareEnabled = useRealtimeKitSelector(
    (m) => m.self.screenShareEnabled,
  );


  const toggleScreenShare = async () => {
    if (screenShareEnabled) {
      await meeting.self.disableScreenShare();
    } else {
      await meeting.self.enableScreenShare();
    }
  };


  return (
    <button onClick={toggleScreenShare}>
      {screenShareEnabled ? "Stop Sharing" : "Share Screen"}
    </button>
  );
}
```

**Kotlin**

```kotlin
// Enable screen share
meeting.localUser.enableScreenShare()


// Disable screen share
meeting.localUser.disableScreenShare()


// Check current status
val isScreenShareEnabled = meeting.localUser.screenShareEnabled
```

Android API 14 and above

Declare the following permission in your app's AndroidManifest.xml to use screenshare on Android devices running Android API 14 and above:

```plaintext
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
```

Adding this permission requires extra steps on Google Play Console. Refer to [Google's documentation ↗](https://support.google.com/googleplay/android-developer/answer/13392821?hl=en#declare) for more information.

**Swift**

```swift
// Enable screen share
let err: ScreenShareError? = meeting.localUser.enableScreenShare()


// Disable screen share
meeting.localUser.disableScreenShare()
```

Refer to the [Screen Share Setup (iOS)](#screen-share-setup-ios) section for platform-specific configuration.

```jsx
function ScreenShareControls() {
  const [meeting] = useRealtimeKitClient();
  const screenShareEnabled = useRealtimeKitSelector(
    (m) => m.self.screenShareEnabled,
  );


  const toggleScreenShare = async () => {
    if (screenShareEnabled) {
      await meeting.self.disableScreenShare();
    } else {
      await meeting.self.enableScreenShare();
    }
  };


  return (
    <TouchableHighlight onPress={toggleScreenShare}>
      <Text>{screenShareEnabled ? "Stop Sharing" : "Share Screen"}</Text>
    </TouchableHighlight>
  );
}
```

**Dart**

```dart
// Enable screen share
meeting.localUser.enableScreenShare();


// Disable screen share
meeting.localUser.disableScreenShare();
```

Platform-specific setup

**Android:** Declare the following permission in your app's AndroidManifest.xml to use screenshare on Android devices running Android API 14 and above:

```xml
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
```

**iOS:** Refer to the [Screen Share Setup (iOS)](#screen-share-setup-ios) section for additional configuration.

### Change display name

Update the display name before joining the meeting:

**JavaScript**

```js
await meeting.self.setName("New Name");
```

Note

Name changes only reflect across all participants if done before joining the meeting.

```jsx
await meeting.self.setName("New Name");
```

Note

Name changes only reflect across all participants if done before joining the meeting.

**Kotlin**

```kotlin
meeting.localUser.setDisplayName("New Name")
```

Note

Name changes only reflect across all participants if done before joining the meeting.

**Swift**

```swift
meeting.localUser.setDisplayName(name: "New Name")
```

Note

Name changes only reflect across all participants if done before joining the meeting.

```jsx
await meeting.self.setName("New Name");
```

Note

Name changes only reflect across all participants if done before joining the meeting.

**Dart**

```dart
if (meeting.permissions.miscellaneous.canEditDisplayName) {
  meeting.localUser.setDisplayName("New Name");
}
```

Note

Name changes only reflect across all participants if done before joining the meeting and the local user has preset permission to change the name.

## Manage media devices

### Get available devices

**JavaScript**

```js
// Get all media devices
const devices = await meeting.self.getAllDevices();


// Get all audio input devices (microphones)
const audioDevices = await meeting.self.getAudioDevices();


// Get all video input devices (cameras)
const videoDevices = await meeting.self.getVideoDevices();


// Get all audio output devices (speakers)
const speakerDevices = await meeting.self.getSpeakerDevices();


// Get device by ID
const device = await meeting.self.getDeviceById("device-id", "audio");


// Get current devices being used
const currentDevices = meeting.self.getCurrentDevices();
// Returns: { audio: MediaDeviceInfo, video: MediaDeviceInfo, speaker: MediaDeviceInfo }
```

```jsx
import { useRealtimeKitClient } from "@cloudflare/realtimekit-react";
import { useState, useEffect } from "react";


function DeviceSelector() {
  const [meeting] = useRealtimeKitClient();
  const [audioDevices, setAudioDevices] = useState([]);
  const [videoDevices, setVideoDevices] = useState([]);


  useEffect(() => {
    if (!meeting) return;


    const loadDevices = async () => {
      const audio = await meeting.self.getAudioDevices();
      const video = await meeting.self.getVideoDevices();
      setAudioDevices(audio);
      setVideoDevices(video);
    };


    loadDevices();
  }, [meeting]);


  const handleDeviceChange = async (device) => {
    await meeting.self.setDevice(device);
  };


  return (
    <div>
      <select
        onChange={(e) => {
          const device = audioDevices.find(
            (d) => d.deviceId === e.target.value,
          );
          handleDeviceChange(device);
        }}
      >
        {audioDevices.map((device) => (
          <option key={device.deviceId} value={device.deviceId}>
            {device.label}
          </option>
        ))}
      </select>
    </div>
  );
}
```

Get current devices being used:

```jsx
const currentDevices = meeting.self.getCurrentDevices();
// Returns: { audio: MediaDeviceInfo, video: MediaDeviceInfo, speaker: MediaDeviceInfo }
```

**Kotlin**

```kotlin
// Get all audio devices
val audioDevices: List<AudioDevice> = meeting.localUser.getAudioDevices()


// Get all video devices
val videoDevices: List<VideoDevice> = meeting.localUser.getVideoDevices()


// Get currently selected audio device
val selectedAudioDevice: AudioDevice = meeting.localUser.getSelectedAudioDevice()


// Get currently selected video device
val selectedVideoDevice: VideoDevice = meeting.localUser.getSelectedVideoDevice()
```

**Swift**

```swift
// Get all audio devices
let audioDevices = meeting.localUser.getAudioDevices()


// Get all video devices
let videoDevices = meeting.localUser.getVideoDevices()


// Get currently selected audio device
let selectedAudioDevice = meeting.localUser.getSelectedAudioDevice()


// Get currently selected video device
let selectedVideoDevice = meeting.localUser.getSelectedVideoDevice()
```

```jsx
import { useRealtimeKitClient } from "@cloudflare/realtimekit-react-native";
import { useState, useEffect } from "react";
import { FlatList, TouchableHighlight, Text, View } from "react-native";


function DeviceSelector() {
  const [meeting] = useRealtimeKitClient();
  const [audioDevices, setAudioDevices] = useState([]);
  const [videoDevices, setVideoDevices] = useState([]);


  useEffect(() => {
    if (!meeting) return;


    const loadDevices = async () => {
      const audio = await meeting.self.getAudioDevices();
      const video = await meeting.self.getVideoDevices();
      setAudioDevices(audio);
      setVideoDevices(video);
    };


    loadDevices();
  }, [meeting]);


  const handleDeviceChange = async (device) => {
    await meeting.self.setDevice(device);
  };


  return (
    <View>
      <FlatList
        data={audioDevices}
        renderItem={({ item }) => (
          <TouchableHighlight onPress={() => handleDeviceChange(item)}>
            <Text>{item.label}</Text>
          </TouchableHighlight>
        )}
        keyExtractor={(item) => item.deviceId}
      />
    </View>
  );
}
```

Get current devices being used:

```jsx
const currentDevices = meeting.self.getCurrentDevices();
// Returns: { audio: MediaDeviceInfo, video: MediaDeviceInfo, speaker: MediaDeviceInfo }
```

**Dart**

```dart
// Get all audio devices
final audioDevices = await meeting.localUser.getAudioDevices();


// Get all video devices
final videoDevices = await meeting.localUser.getVideoDevices();


// Get currently selected audio device
final selectedAudioDevice = meeting.localUser.getSelectedAudioDevice();


// Get currently selected video device
final selectedVideoDevice = meeting.localUser.getSelectedVideoDevice();
```

### Change device

Switch to a different media device:

**JavaScript**

```js
// Get all devices
const devices = await meeting.self.getAllDevices();


// Set a specific device (replaces device of the same kind)
await meeting.self.setDevice(devices[0]);
```

Use the device selector example from the previous section. The `handleDeviceChange` function demonstrates how to switch devices.

**Kotlin**

```kotlin
// Get all audio devices
val audioDevices = meeting.localUser.getAudioDevices()


// Set audio device
meeting.localUser.setAudioDevice(audioDevices[0])


// Get all video devices
val videoDevices = meeting.localUser.getVideoDevices()


// Set video device
meeting.localUser.setVideoDevice(videoDevices[0])


// Switch between front and back camera on devices with 2 cameras
meeting.localUser.switchCamera()
```

**Swift**

```swift
// Set audio device
meeting.localUser.setAudioDevice(device)


// Set video device
meeting.localUser.setVideoDevice(videoDevice: device)


// Switch between front and back camera
meeting.localUser.switchCamera()
```

Use the device selector example from the previous section. The `handleDeviceChange` function demonstrates how to switch devices.

**JavaScript**

```js
const handleDeviceChange = async (device) => {
  await meeting.self.setDevice(device);
};
```

**Dart**

```dart
// Get all available audio devices
final audioDevices = await meeting.localUser.getAudioDevices();


// Switch audio device
await meeting.localUser.setAudioDevice(audioDevices[1]);


// Get all available video devices
final videoDevices = await meeting.localUser.getVideoDevices();


// Switch video device
await meeting.localUser.setVideoDevice(videoDevices[1]);


// Switch between available camera sources
meeting.localUser.switchCamera();
```

## Display local video

### Register video element

Attach the local video track to a `<video>` element:

```html
<video id="local-video" autoplay playsinline></video>
```

**JavaScript**

```js
const videoElement = document.getElementById("local-video");


// Register the video element to display video
meeting.self.registerVideoElement(videoElement);


// For local preview (not sent to other users), pass true as second argument
meeting.self.registerVideoElement(videoElement, true);
```

### Deregister video element

Remove the video element when no longer needed:

**JavaScript**

```js
meeting.self.deregisterVideoElement(videoElement);
```

### Use UI Kit component

Display local video with the UI Kit video tile component:

```jsx
import { RtkParticipantTile } from "@cloudflare/realtimekit-react-ui";
import { useRealtimeKitSelector } from "@cloudflare/realtimekit-react";


function LocalVideo() {
  const localUser = useRealtimeKitSelector((m) => m.self);


  return <RtkParticipantTile participant={localUser} />;
}
```

### Manage video element manually

Create custom video element implementations:

```jsx
import {
  useRealtimeKitClient,
  useRealtimeKitSelector,
} from "@cloudflare/realtimekit-react";
import { useEffect, useRef } from "react";


function LocalVideoCustom() {
  const [meeting] = useRealtimeKitClient();
  const videoEnabled = useRealtimeKitSelector((m) => m.self.videoEnabled);
  const videoTrack = useRealtimeKitSelector((m) => m.self.videoTrack);
  const videoRef = useRef(null);


  useEffect(() => {
    if (!videoRef.current || !meeting) return;


    // Register video element
    meeting.self.registerVideoElement(videoRef.current);


    return () => {
      // Cleanup: deregister on unmount
      meeting.self.deregisterVideoElement(videoRef.current);
    };
  }, [meeting]);


  return (
    <video
      ref={videoRef}
      autoPlay
      playsInline
      muted
      style={{ display: videoEnabled ? "block" : "none" }}
    />
  );
}
```

### Get video view

Retrieve a self-preview video view that renders the local camera stream:

**Kotlin**

```kotlin
// Get the self-preview video view
val videoView = meeting.localUser.getSelfPreview()
```

For rendering other participants' video, use:

**Kotlin**

```kotlin
// Get video view for camera stream
val participantVideoView = participant.getVideoView()


// Get video view for screenshare stream
val screenshareView = participant.getScreenShareVideoView()
```

### Manage lifecycle

Control video rendering with lifecycle methods:

**Kotlin**

```kotlin
// Start rendering video
videoView.renderVideo()


// Stop rendering video (but keep the view)
videoView.stopVideoRender()


// Release native resources when done
videoView.release()
```

### Complete Example

**Kotlin**

```kotlin
import android.os.Bundle
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import io.dyte.core.VideoView


class MainActivity : AppCompatActivity() {
    private lateinit var videoView: VideoView


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)


        // Get the self-preview video view
        videoView = meeting.localUser.getSelfPreview()


        // Add to your layout
        val container = findViewById<FrameLayout>(R.id.video_container)
        container.addView(videoView)


        // Start rendering
        videoView.renderVideo()
    }


    override fun onPause() {
        super.onPause()
        // Stop rendering when activity is paused
        videoView.stopVideoRender()
    }


    override fun onResume() {
        super.onResume()
        // Resume rendering when activity is resumed
        videoView.renderVideo()
    }


    override fun onDestroy() {
        super.onDestroy()
        // Clean up resources
        videoView.release()
    }
}
```

### Get video view

Retrieve video views that render the participant's video streams:

**Swift**

```swift
// Get video view for local camera stream
let videoView = meeting.localUser.getVideoView()


// Get video view for screenshare stream
let screenshareView = meeting.localUser.getScreenShareVideoView()
```

### Manage lifecycle

The `UIView` handles its own lifecycle automatically and cleans up native resources when it exits the current window. No manual cleanup is required.

### Example

**Swift**

```swift
import UIKit
import RealtimeKit


class VideoViewController: UIViewController {
    private var videoView: UIView?


    override func viewDidLoad() {
        super.viewDidLoad()


        // Get the video view for local camera
        videoView = meeting.localUser.getVideoView()


        // Add to your view hierarchy
        if let videoView = videoView {
            videoView.frame = view.bounds
            videoView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            view.addSubview(videoView)
        }
    }
}
```

For screenshare:

**Swift**

```swift
// Get and display screenshare view
let screenshareView = meeting.localUser.getScreenShareVideoView()
if let screenshareView = screenshareView {
    screenshareView.frame = view.bounds
    screenshareView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    view.addSubview(screenshareView)
}
```

```jsx
import React from "react";
import { useRealtimeKitSelector } from "@cloudflare/realtimekit-react-native";
import { MediaStream, RTCView } from "@cloudflare/react-native-webrtc";


export default function VideoView() {
  const { videoTrack } = useRealtimeKitSelector(
    (m) => m.participants.active,
  ).toArray()[0];
  const stream = new MediaStream(undefined);
  stream.addTrack(videoTrack);
  return (
    <RTCView
      objectFit={"cover"}
      style={{ flex: 1 }}
      streamURL={stream.toURL()}
      mirror={true}
      zOrder={1}
    />
  );
}
```

### VideoView widget

Display video streams with the `VideoView` widget:

**Dart**

```dart
import 'package:realtimekit_core/realtimekit_core.dart';
import 'package:flutter/material.dart';


class LocalVideoView extends StatelessWidget {
  final RtkMeetingParticipant localUser;


  const LocalVideoView({Key? key, required this.localUser}) : super(key: key);


  @override
  Widget build(BuildContext context) {
    return VideoView(
      meetingParticipant: localUser,
      isSelfParticipant: true,
    );
  }
}
```

### VideoView parameters

The `VideoView` widget accepts the following parameters:

* `meetingParticipant` (required): The `RtkMeetingParticipant` whose video should be displayed
* `isSelfParticipant` (optional): Set to `true` for the local participant's self-preview, defaults to `false`
* `key` (optional): Widget key for Flutter's widget tree management

### Example

**Dart**

```dart
import 'package:flutter/material.dart';
import 'package:realtimekit_core/realtimekit_core.dart';


class MeetingScreen extends StatelessWidget {
  final RtkMeeting meeting;


  const MeetingScreen({Key? key, required this.meeting}) : super(key: key);


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Video Preview')),
      body: Container(
        child: VideoView(
          meetingParticipant: meeting.localUser,
          isSelfParticipant: true,
        ),
      ),
    );
  }
}
```

For displaying other participants' video:

**Dart**

```dart
// Display remote participant video
VideoView(
  meetingParticipant: remoteParticipant,
  isSelfParticipant: false,
)
```

The `VideoView` widget automatically handles video rendering and resource cleanup based on Flutter's widget lifecycle.

## Screen share setup (iOS)

### Add broadcast upload extension

In Xcode, add a Broadcast Upload Extension through `File` → `New` → `Target`. Choose `iOS` → `Broadcast Upload Extension` and fill out the required information.

### Configure app groups

Add your extension to an app group:

1. Go to your extension's target in the project
2. In the Signings & Capabilities tab, click the + button in the top left
3. Add App Groups
4. Add App Groups to your main app as well, ensuring the App Group identifier is the same for both

### Configure SampleHandler

Edit your SampleHandler class:

**Swift**

```swift
import RealtimeKit


class SampleHandler: RtkSampleHandler {}
```

### Update Info.plist

Ensure **both** App and Extension Info.plist files contain these keys:

```xml
<key>RTKRTCAppGroupIdentifier</key>
<string>(name of the group you have created)</string>
```

Add this key inside the Info.plist of the main App:

```xml
<key>RTKRTCScreenSharingExtension</key>
<string>(Bundle Identifier of the Broadcast upload extension)</string>
```

### Enable screen share

Launch the broadcast extension and enable screen share:

**Swift**

```swift
meeting.localUser.enableScreenShare()
```

To stop the screen share:

**Swift**

```swift
meeting.localUser.disableScreenShare()
```

### Add broadcast upload extension

In Xcode, add a Broadcast Upload Extension through `File` → `New` → `Target`. Choose `iOS` → `Broadcast Upload Extension` and fill out the required information.

### Configure app groups

Add your extension to an app group:

1. Go to your extension's target in the project
2. In the Signings & Capabilities tab, click the + button in the top left
3. Add App Groups
4. Add App Groups to your main app as well, ensuring the App Group identifier is the same for both

### Configure SampleHandler

1. Place the `RtkSampleHandler.swift` file from [GitHub ↗](https://github.com/dyte-io/iOS-ScreenShare/blob/main/RtkSampleHandler.swift) in the `ios/<screenshare-folder>/` folder
2. Create or replace `SampleHandler.swift`:

**Swift**

```swift
import ReplayKit


class SampleHandler: RtkSampleHandler {
}
```

### Update Info.plist

Ensure **both** App and Extension Info.plist files contain these keys:

```xml
<key>RTKRTCAppGroupIdentifier</key>
<string>(name of the group you have created)</string>
```

Add this key inside the Info.plist of the main App:

```xml
<key>RTKRTCScreenSharingExtension</key>
<string>(Bundle Identifier of the Broadcast upload extension)</string>
```

### Enable screen share

Launch the broadcast extension and enable screen share:

**Dart**

```dart
meeting.localUser.enableScreenShare()
```

To stop the screen share:

**Dart**

```dart
meeting.localUser.disableScreenShare()
```

### Add broadcast upload extension

In Xcode, add a Broadcast Upload Extension through `File` → `New` → `Target`. Choose `iOS` → `Broadcast Upload Extension` and fill out the required information.

### Configure app groups

Add your extension to an app group:

1. Go to your extension's target in the project.
2. In the **Signing & Capabilities** tab, click **+** and add **App Groups**.
3. Add the same App Group to your main app target, using the same identifier for both.

### Add screenshare sources via Podfile

The SDK ships a Ruby helper script that adds the required screenshare Swift source files directly to your Broadcast Upload Extension target. Add the following to your `Podfile`:

```ruby
# Add this line at the top
require Pod::Executable.execute_command('node', ['-p',
  'require.resolve(
    "@cloudflare/realtimekit-react-native/ios/scripts/screenshare_sources.rb",
    {paths: [process.argv[1]]},
  )', __dir__]).strip


target 'YourApp' do
  ...
  post_install do |installer|
    ...
    # Add this line here
    add_screenshare_sources(
      installer,
      project_name: 'YourApp',              # your Xcode project name (without .xcodeproj)
      extension_target_name: 'YourAppScreenshare' # your Broadcast Upload Extension target name
    )
    ...
  end
end


target 'YourAppScreenshare' do
# Remove the old steps added if any
end
```

Then run:

```bash
pod install
```

### Configure SampleHandler

In your Broadcast Upload Extension, edit `SampleHandler.swift`:

**Swift**

```swift
class SampleHandler: RTKScreenshareHandler {
  override init() {
    super.init(
      appGroupIdentifier: "<YOUR_APP_GROUP_IDENTIFIER>",
      bundleIdentifier: "<YOUR_APP_BUNDLE_IDENTIFIER>"
    )
  }
}
```

Replace `<YOUR_APP_GROUP_IDENTIFIER>` with the App Group you created (for example, `group.com.yourcompany.yourapp`) and `<YOUR_APP_BUNDLE_IDENTIFIER>` with your main app's bundle identifier.

### Update Info.plist

Add the following key to **both** your main app and extension `Info.plist` files:

```xml
<key>RTCAppGroupIdentifier</key>
<string>(YOUR_APP_GROUP_IDENTIFIER)</string>
```

Add this key to the main app `Info.plist` only:

```xml
<key>RTCAppScreenSharingExtension</key>
<string>(Bundle Identifier of the Broadcast upload extension)</string>
```

### Enable screen share

**JavaScript**

```js
meeting.self.enableScreenShare();
```

To stop the screen share:

**JavaScript**

```js
meeting.self.disableScreenShare();
```

## Events

### Room joined

Fires when the local user joins the meeting:

**JavaScript**

```js
meeting.self.on("roomJoined", () => {
  console.log("Successfully joined the meeting");
});
```

```jsx
const roomJoined = useRealtimeKitSelector((m) => m.self.roomJoined);


useEffect(() => {
  if (roomJoined) {
    console.log("Successfully joined the meeting");
  }
}, [roomJoined]);
```

Or use event listener:

```jsx
useEffect(() => {
  if (!meeting) return;


  const handleRoomJoined = () => {
    console.log("Successfully joined the meeting");
  };


  meeting.self.on("roomJoined", handleRoomJoined);


  return () => {
    meeting.self.off("roomJoined", handleRoomJoined);
  };
}, [meeting]);
```

Android SDK uses a different event model. Monitor `roomJoined` property changes or use listeners for state changes.

iOS SDK uses a different event model. Monitor `roomJoined` property changes or use listeners for state changes.

```jsx
const roomJoined = useRealtimeKitSelector((m) => m.self.roomJoined);


useEffect(() => {
  if (roomJoined) {
    console.log("Successfully joined the meeting");
  }
}, [roomJoined]);
```

Or use event listener:

```jsx
useEffect(() => {
  if (!meeting) return;


  const handleRoomJoined = () => {
    console.log("Successfully joined the meeting");
  };


  meeting.self.on("roomJoined", handleRoomJoined);


  return () => {
    meeting.self.off("roomJoined", handleRoomJoined);
  };
}, [meeting]);
```

Flutter SDK uses a different event model. Monitor `roomJoined` property changes or use listeners for state changes.

### Room left

Fires when the local user leaves the meeting:

**JavaScript**

```js
meeting.self.on("roomLeft", ({ state }) => {
  console.log("Left the meeting with state:", state);


  // Handle different leave states
  if (state === "left") {
    console.log("User voluntarily left");
  } else if (state === "kicked") {
    console.log("User was kicked from the meeting");
  } else if (state === "ended") {
    console.log("Meeting has ended");
  } else if (state === "disconnected") {
    console.log("Lost connection to meeting");
  }
});
```

**Possible state values:** `'left'`, `'kicked'`, `'ended'`, `'rejected'`, `'disconnected'`, `'failed'`

```jsx
const roomJoined = useRealtimeKitSelector((m) => m.self.roomJoined);


useEffect(() => {
  if (!roomJoined) {
    console.log("Left the meeting");
  }
}, [roomJoined]);
```

Or use event listener for detailed state:

```jsx
meeting.self.on("roomLeft", ({ state }) => {
  if (state === "left") {
    console.log("User voluntarily left");
  } else if (state === "kicked") {
    console.log("User was kicked");
  }
});
```

Use `RtkSelfEventListener` to monitor when the local user is removed from the meeting:

**Kotlin**

```kotlin
meeting.addSelfEventListener(object : RtkSelfEventListener {
    override fun onRemovedFromMeeting() {
        // display alert that user is no longer in the meeting
    }
})
```

iOS SDK uses a different event model. Monitor `roomJoined` property changes or use listeners for state changes.

```jsx
const roomJoined = useRealtimeKitSelector((m) => m.self.roomJoined);


useEffect(() => {
  if (!roomJoined) {
    console.log("Left the meeting");
  }
}, [roomJoined]);
```

Or use event listener for detailed state:

```jsx
meeting.self.on("roomLeft", ({ state }) => {
  if (state === "left") {
    console.log("User voluntarily left");
  } else if (state === "kicked") {
    console.log("User was kicked");
  }
});
```

**Dart**

```dart
class MeetingSelfListener extends RtkSelfEventListener {
  @override
  void onRemovedFromMeeting() {
    // User was removed from the meeting (kicked or meeting ended)
    // Display alert or navigate to exit screen
  }
}


// Add the listener
meeting.addSelfEventListener(MeetingSelfListener());
```

### Video update

Fires when video is enabled or disabled:

**JavaScript**

```js
meeting.self.on("videoUpdate", ({ videoEnabled, videoTrack }) => {
  console.log("Video state:", videoEnabled);


  if (videoEnabled) {
    // Video track is available, can display it
    const videoElement = document.getElementById("my-video");
    const stream = new MediaStream();
    stream.addTrack(videoTrack);
    videoElement.srcObject = stream;
    videoElement.play();
  }
});
```

```jsx
const videoEnabled = useRealtimeKitSelector((m) => m.self.videoEnabled);
const videoTrack = useRealtimeKitSelector((m) => m.self.videoTrack);


useEffect(() => {
  if (videoEnabled && videoTrack) {
    console.log("Video is enabled");
    // Handle video track
  }
}, [videoEnabled, videoTrack]);
```

**Kotlin**

```kotlin
meeting.addSelfEventListener(object : RtkSelfEventListener {
    override fun onVideoUpdate(isEnabled: Boolean) {
        if (isEnabled) {
            // video is enabled, other participants can see local user
        } else {
            // video is disabled, other participants cannot see local user
        }
    }
})
```

**Swift**

```swift
extension MeetingViewModel: RtkSelfEventListener {
    func onVideoUpdate(isEnabled: Bool) {
        if (isEnabled) {
            // video is enabled, other participants can see local user
        } else {
            // video is disabled, other participants cannot see local user
        }
    }
}
```

```jsx
const videoEnabled = useRealtimeKitSelector((m) => m.self.videoEnabled);
const videoTrack = useRealtimeKitSelector((m) => m.self.videoTrack);


useEffect(() => {
  if (videoEnabled && videoTrack) {
    console.log("Video is enabled");
    // Handle video track
  }
}, [videoEnabled, videoTrack]);
```

Flutter SDK uses a different event model. Monitor `videoEnabled` property changes.

### Audio update

Fires when audio is enabled or disabled:

**JavaScript**

```js
meeting.self.on("audioUpdate", ({ audioEnabled, audioTrack }) => {
  console.log("Audio state:", audioEnabled);


  if (audioEnabled) {
    // Audio track is available
    console.log("Microphone is on");
  }
});
```

```jsx
const audioEnabled = useRealtimeKitSelector((m) => m.self.audioEnabled);
const audioTrack = useRealtimeKitSelector((m) => m.self.audioTrack);


useEffect(() => {
  if (audioEnabled && audioTrack) {
    console.log("Audio is enabled");
    // Handle audio track
  }
}, [audioEnabled, audioTrack]);
```

**Kotlin**

```kotlin
meeting.addSelfEventListener(object : RtkSelfEventListener {
    override fun onAudioUpdate(isEnabled: Boolean) {
        if (isEnabled) {
            // audio is enabled, other participants can hear local user
        } else {
            // audio is disabled, other participants cannot hear local user
        }
    }
})
```

**Swift**

```swift
extension MeetingViewModel: RtkSelfEventListener {
    func onAudioUpdate(isEnabled: Bool) {
        if (isEnabled) {
            // audio is enabled, other participants can hear local user
        } else {
            // audio is disabled, other participants cannot hear local user
        }
    }
}
```

```jsx
const audioEnabled = useRealtimeKitSelector((m) => m.self.audioEnabled);
const audioTrack = useRealtimeKitSelector((m) => m.self.audioTrack);


useEffect(() => {
  if (audioEnabled && audioTrack) {
    console.log("Audio is enabled");
    // Handle audio track
  }
}, [audioEnabled, audioTrack]);
```

Flutter SDK uses a different event model. Monitor `audioEnabled` property changes.

### Screen share update

Fires when screen sharing starts or stops:

**JavaScript**

```js
meeting.self.on(
  "screenShareUpdate",
  ({ screenShareEnabled, screenShareTracks }) => {
    console.log("Screen share state:", screenShareEnabled);


    if (screenShareEnabled) {
      // Screen share tracks are available
      const screenElement = document.getElementById("my-screen-share");
      const stream = new MediaStream();
      stream.addTrack(screenShareTracks.video);
      if (screenShareTracks.audio) {
        stream.addTrack(screenShareTracks.audio);
      }
      screenElement.srcObject = stream;
      screenElement.play();
    }
  },
);
```

```jsx
const screenShareEnabled = useRealtimeKitSelector(
  (m) => m.self.screenShareEnabled,
);
const screenShareTracks = useRealtimeKitSelector(
  (m) => m.self.screenShareTracks,
);


useEffect(() => {
  if (screenShareEnabled && screenShareTracks) {
    console.log("Screen sharing is active");
    // Handle screen share tracks
  }
}, [screenShareEnabled, screenShareTracks]);
```

**Kotlin**

```kotlin
meeting.addSelfEventListener(object : RtkSelfEventListener {
    override fun onScreenShareStartFailed(reason: String) {
        // screen share failed to start
    }


    override fun onScreenShareUpdate(isEnabled: Boolean) {
        if (isEnabled) {
            // screen share is enabled
        } else {
            // screen share is disabled
        }
    }
})
```

**Swift**

```swift
meeting.addSelfEventListener(self)


extension MeetingViewModel: RtkSelfEventListener {
    func onRemovedFromMeeting() {
        // User was removed from the meeting (kicked or meeting ended)
        // Display alert or navigate to exit screen
    }


    func onMeetingRoomDisconnected() {
        // Lost connection to the meeting room
        // Display reconnection UI or error message
    }
}
```

You can also monitor the `roomJoined` property for state changes:

**Swift**

```swift
let isInMeeting = meeting.localUser.roomJoined
```

```jsx
const screenShareEnabled = useRealtimeKitSelector(
  (m) => m.self.screenShareEnabled,
);
const screenShareTracks = useRealtimeKitSelector(
  (m) => m.self.screenShareTracks,
);


useEffect(() => {
  if (screenShareEnabled && screenShareTracks) {
    console.log("Screen sharing is active");
    // Handle screen share tracks
  }
}, [screenShareEnabled, screenShareTracks]);
```

Flutter SDK uses a different event model. Monitor `screenShareEnabled` property changes.

### Device update

Fires when the active device changes:

**JavaScript**

```js
meeting.self.on("deviceUpdate", ({ device }) => {
  // Handle device change
  if (device.kind === "audioinput") {
    console.log("Microphone changed:", device.label);
  } else if (device.kind === "videoinput") {
    console.log("Camera changed:", device.label);
  } else if (device.kind === "audiooutput") {
    console.log("Speaker changed:", device.label);
  }
});
```

```jsx
useEffect(() => {
  if (!meeting) return;


  const handleDeviceUpdate = ({ device }) => {
    if (device.kind === "audioinput") {
      console.log("Microphone changed:", device.label);
    } else if (device.kind === "videoinput") {
      console.log("Camera changed:", device.label);
    }
  };


  meeting.self.on("deviceUpdate", handleDeviceUpdate);


  return () => {
    meeting.self.off("deviceUpdate", handleDeviceUpdate);
  };
}, [meeting]);
```

**Kotlin**

```kotlin
meeting.self.addSelfEventListener(object : RtkSelfEventListener() {
    override fun onAudioDeviceChanged(device: AudioDevice) {
        // Handle audio device change
        println("Audio device changed: ${device.label}")
    }


    override fun onVideoDeviceChanged(device: VideoDevice) {
        // Handle video device change
        println("Video device changed: ${device.label}")
    }
})
```

**Swift**

```swift
meeting.self.addSelfEventListener(self)


// RtkSelfEventListener implementation
func onAudioDeviceChanged(device: AudioDevice) {
    // Handle audio device change
    print("Audio device changed: \(device.label)")
}


func onVideoDeviceChanged(device: VideoDevice) {
    // Handle video device change
    print("Video device changed: \(device.label)")
}
```

```jsx
useEffect(() => {
  if (!meeting) return;


  const handleDeviceUpdate = ({ device }) => {
    if (device.kind === "audioinput") {
      console.log("Microphone changed:", device.label);
    } else if (device.kind === "videoinput") {
      console.log("Camera changed:", device.label);
    }
  };


  meeting.self.on("deviceUpdate", handleDeviceUpdate);


  return () => {
    meeting.self.off("deviceUpdate", handleDeviceUpdate);
  };
}, [meeting]);
```

**Dart**

```dart
class DeviceChangeListener extends RtkSelfEventListener {
  @override
  void onAudioDeviceChanged(AudioDevice audioDevice) {
    // Handle audio device change
    print('Audio device changed: ${audioDevice.label}');
  }


  @override
  void onVideoDeviceChanged(VideoDevice videoDevice) {
    // Handle video device change
    print('Video device changed: ${videoDevice.label}');
  }
}


// Add the listener
meeting.addSelfEventListener(DeviceChangeListener());
```

### Device List Update

Triggered when the list of available devices changes (device plugged in or out):

**JavaScript**

```js
meeting.self.on("deviceListUpdate", ({ added, removed, devices }) => {
  console.log("Device list updated");
  console.log("Added devices:", added);
  console.log("Removed devices:", removed);
  console.log("All devices:", devices);
});
```

```jsx
useEffect(() => {
  if (!meeting) return;


  const handleDeviceListUpdate = ({ added, removed, devices }) => {
    console.log("Device list updated");
    console.log("Added devices:", added);
    console.log("Removed devices:", removed);
    console.log("All devices:", devices);
  };


  meeting.self.on("deviceListUpdate", handleDeviceListUpdate);


  return () => {
    meeting.self.off("deviceListUpdate", handleDeviceListUpdate);
  };
}, [meeting]);
```

**Kotlin**

```kotlin
meeting.addSelfEventListener(object : RtkSelfEventListener {
    // Triggered when audio devices are added or removed
    override fun onAudioDevicesUpdated() {
        val audioDevices = meeting.localUser.getAudioDevices()
        // Update UI with new audio device list
    }
})
```

**Swift**

```swift
meeting.addSelfEventListener(object: RtkSelfEventListener {
    // Triggered when audio devices are added or removed
    func onAudioDevicesUpdated() {
        let audioDevices = meeting.localUser.getAudioDevices()
        // Update UI with new audio device list
    }
})
```

```jsx
useEffect(() => {
  if (!meeting) return;


  const handleDeviceListUpdate = ({ added, removed, devices }) => {
    console.log("Device list updated");
    console.log("Added devices:", added);
    console.log("Removed devices:", removed);
    console.log("All devices:", devices);
  };


  meeting.self.on("deviceListUpdate", handleDeviceListUpdate);


  return () => {
    meeting.self.off("deviceListUpdate", handleDeviceListUpdate);
  };
}, [meeting]);
```

**Dart**

```dart
class DeviceListListener extends RtkSelfEventListener {
  final RealtimekitClient meeting;


  DeviceListListener(this.meeting);


  @override
  void onAudioDevicesUpdated(List<AudioDevice> devices) {
    // Triggered when audio devices are added or removed
    // Update UI with new audio device list
  }


  @override
  void onVideoDeviceChanged(VideoDevice videoDevice) {
    // Handle video device change
    print('Video device changed to: ${videoDevice.label}');
  }
}


// Add the listener
meeting.addSelfEventListener(DeviceListListener(meeting));
```

### Network Quality Score

Monitor your own network quality:

**JavaScript**

```js
meeting.self.on(
  "mediaScoreUpdate",
  ({ kind, isScreenshare, score, scoreStats }) => {
    if (kind === "video") {
      console.log(
        `Your ${isScreenshare ? "screenshare" : "video"} quality score is`,
        score,
      );
    }


    if (kind === "audio") {
      console.log("Your audio quality score is", score);
    }


    if (score < 5) {
      console.log("Your media quality is poor");
    }
  },
);
```

The `scoreStats` object provides detailed statistics:

**JavaScript**

```js
// Audio Producer
{
  "kind": "audio",
  "isScreenshare": false,
  "score": 10,
  "participantId": "meeting.self.id",
  "scoreStats": {
    "score": 10,
    "bitrate": 22452,
    "packetsLostPercentage": 0,
    "jitter": 0,
    "isScreenShare": false
  }
}


// Video Producer
{
  "kind": "video",
  "isScreenshare": false,
  "score": 10,
  "participantId": "meeting.self.id",
  "scoreStats": {
    "score": 10,
    "frameWidth": 640,
    "frameHeight": 480,
    "framesPerSecond": 24,
    "jitter": 0,
    "isScreenShare": false,
    "packetsLostPercentage": 0,
    "bitrate": 576195,
    "cpuLimitations": false,
    "bandwidthLimitations": false
  }
}
```

```jsx
useEffect(() => {
  if (!meeting) return;


  const handleMediaScoreUpdate = ({
    kind,
    isScreenshare,
    score,
    scoreStats,
  }) => {
    if (kind === "video") {
      console.log(
        `Your ${isScreenshare ? "screenshare" : "video"} quality score is`,
        score,
      );
    }


    if (score < 5) {
      console.log("Your media quality is poor");
    }
  };


  meeting.self.on("mediaScoreUpdate", handleMediaScoreUpdate);


  return () => {
    meeting.self.off("mediaScoreUpdate", handleMediaScoreUpdate);
  };
}, [meeting]);
```

Android SDK does not currently expose network quality scores.

iOS SDK does not currently expose network quality scores.

```jsx
useEffect(() => {
  if (!meeting) return;


  const handleMediaScoreUpdate = ({
    kind,
    isScreenshare,
    score,
    scoreStats,
  }) => {
    if (kind === "video") {
      console.log(
        `Your ${isScreenshare ? "screenshare" : "video"} quality score is`,
        score,
      );
    }


    if (score < 5) {
      console.log("Your media quality is poor");
    }
  };


  meeting.self.on("mediaScoreUpdate", handleMediaScoreUpdate);


  return () => {
    meeting.self.off("mediaScoreUpdate", handleMediaScoreUpdate);
  };
}, [meeting]);
```

Flutter SDK does not currently expose network quality scores.

### Permission Updates

Triggered when permissions are updated dynamically:

**JavaScript**

```js
// Listen to specific permission updates
meeting.self.permissions.on("chatUpdate", () => {
  console.log("Chat permissions updated");
  // Check meeting.self.permissions for updated permissions
});


meeting.self.permissions.on("pollsUpdate", () => {
  console.log("Polls permissions updated");
});


meeting.self.permissions.on("pluginsUpdate", () => {
  console.log("Plugins permissions updated");
});


// Listen to all permission updates
meeting.self.permissions.on("*", () => {
  console.log("Permissions updated");
});
```

Monitor permissions using selectors:

```jsx
const permissions = useRealtimeKitSelector((m) => m.self.permissions);


useEffect(() => {
  console.log("Permissions updated:", permissions);
}, [permissions]);
```

Android SDK uses a different permissions model. Refer to the Android-specific documentation.

iOS SDK uses a different permissions model. Refer to the iOS-specific documentation.

Monitor permissions using selectors:

```jsx
const permissions = useRealtimeKitSelector((m) => m.self.permissions);


useEffect(() => {
  console.log("Permissions updated:", permissions);
}, [permissions]);
```

Flutter SDK uses a different permissions model. Refer to the Flutter-specific documentation.

### Media Permission Errors

Triggered when media permissions are denied or media capture fails:

**JavaScript**

```js
meeting.self.on("mediaPermissionError", ({ message, kind }) => {
  console.log(`Failed to capture ${kind}: ${message}`);


  // Handle different error types
  if (message === "DENIED") {
    console.log("User denied permission");
  } else if (message === "SYSTEM_DENIED") {
    console.log("System denied permission");
  } else if (message === "COULD_NOT_START") {
    console.log("Failed to start media stream");
  }
});
```

**Possible values:**

* `message`: `'DENIED'`, `'SYSTEM_DENIED'`, `'COULD_NOT_START'`
* `kind`: `'audio'`, `'video'`, `'screenshare'`

```jsx
useEffect(() => {
  if (!meeting) return;


  const handlePermissionError = ({ message, kind }) => {
    console.log(`Failed to capture ${kind}: ${message}`);


    if (message === "DENIED") {
      // Show UI to guide user to grant permissions
    }
  };


  meeting.self.on("mediaPermissionError", handlePermissionError);


  return () => {
    meeting.self.off("mediaPermissionError", handlePermissionError);
  };
}, [meeting]);
```

**Kotlin**

```kotlin
meeting.addSelfEventListener(object : RtkSelfEventListener {
    override fun onMeetingRoomJoinedWithoutCameraPermission() {
        // meeting joined without camera permission
    }


    override fun onMeetingRoomJoinedWithoutMicPermission() {
        // meeting joined without microphone permission
    }
})
```

**Swift**

```swift
meeting.addSelfEventListener(self)


extension MeetingViewModel: RtkSelfEventListener {
    func onMeetingRoomJoinedWithoutCameraPermission() {
        // meeting joined without camera permission
    }


    func onMeetingRoomJoinedWithoutMicPermission() {
        // meeting joined without microphone permission
    }
}
```

You can also check permission status using properties:

**Swift**

```swift
let hasCameraPermission = meeting.localUser.isCameraPermissionGranted
let hasMicPermission = meeting.localUser.isMicrophonePermissionGranted
```

```jsx
useEffect(() => {
  if (!meeting) return;


  const handlePermissionError = ({ message, kind }) => {
    console.log(`Failed to capture ${kind}: ${message}`);


    if (message === "DENIED") {
      // Show UI to guide user to grant permissions
    }
  };


  meeting.self.on("mediaPermissionError", handlePermissionError);


  return () => {
    meeting.self.off("mediaPermissionError", handlePermissionError);
  };
}, [meeting]);
```

**Dart**

```dart
class PermissionListener extends RtkSelfEventListener {
  @override
  void onMeetingRoomJoinedWithoutCameraPermission() {
    // Meeting joined without camera permission
  }


  @override
  void onMeetingRoomJoinedWithoutMicPermission() {
    // Meeting joined without microphone permission
  }
}


// Add the listener
meeting.addSelfEventListener(PermissionListener());
```

You can also check permission status using properties:

**Dart**

```dart
final hasCameraPermission = meeting.localUser.isCameraPermissionGranted;
final hasMicPermission = meeting.localUser.isMicrophonePermissionGranted;
```

### Waitlist Status

For meetings with waiting room enabled:

Monitor the `roomState` property for waitlist status. The value `'waitlisted'` indicates the user is in the waiting room.

```jsx
const roomState = useRealtimeKitSelector((m) => m.self.roomState);


useEffect(() => {
  if (roomState === "waitlisted") {
    console.log("Waiting for host to admit you");
  }
}, [roomState]);
```

**Kotlin**

```kotlin
// Get current waitlist status
val waitListStatus = meeting.localUser.waitListStatus


// Listen to waitlist status changes
meeting.addSelfEventListener(object : RtkSelfEventListener {
    override fun onWaitListStatusUpdate(waitListStatus: WaitListStatus) {
        // handle waitlist status here
    }
})
```

**Swift**

```swift
// Get current waitlist status
let waitListStatus = meeting.localUser.waitListStatus


// Listen to waitlist status changes
extension MeetingViewModel: RtkSelfEventListener {
    func onWaitlistedUpdate() {
        // handle waitlist update
    }
}
```

```jsx
const roomState = useRealtimeKitSelector((m) => m.self.roomState);


useEffect(() => {
  if (roomState === "waitlisted") {
    console.log("Waiting for host to admit you");
  }
}, [roomState]);
```

Flutter SDK uses a different event model. Monitor `stageStatus` or relevant properties for waitlist status.

### iOS-Specific Events

The iOS SDK provides additional platform-specific events:

#### Proximity Sensor

Triggered when the proximity sensor detects a change (useful for earpiece detection):

**Swift**

```swift
extension MeetingViewModel: RtkSelfEventListener {
    func onProximityChanged() {
        // Handle proximity sensor change
        // Useful for detecting when device is near user's ear
    }
}
```

#### Webinar Events

For webinar-specific functionality:

**Swift**

```swift
extension MeetingViewModel: RtkSelfEventListener {
    func onWebinarPresentRequestReceived() {
        // Handle request to present in webinar
    }


    func onStoppedPresenting() {
        // Handle stopped presenting in webinar
    }
}
```

#### Room Messages

Listen to broadcast messages in the room:

**Swift**

```swift
extension MeetingViewModel: RtkSelfEventListener {
    func onRoomMessage() {
        // Handle room broadcast message
    }
}
```

## Pin and Unpin

Pin or unpin yourself in the meeting (requires appropriate permissions):

Web SDK does not currently support pinning the local participant.

Web SDK does not currently support pinning the local participant.

Android SDK does not currently support pinning the local participant.

**Swift**

```swift
// Pin yourself
meeting.localUser.pin()


// Unpin yourself
meeting.localUser.unpin()


// Check if pinned
let isPinned = meeting.localUser.isPinned
```

```jsx
// Pin yourself
await meeting.self.pin();


// Unpin yourself
await meeting.self.unpin();


// Check if pinned
const isPinned = meeting.self.isPinned;
```

Flutter SDK does not currently support pinning the local participant.

## Update Media Constraints

Update video or screenshare resolution at runtime:

Web SDK does not currently expose runtime constraint updates for local participant.

Web SDK does not currently expose runtime constraint updates for local participant.

Android SDK does not currently expose runtime constraint updates.

iOS SDK does not currently expose runtime constraint updates.

### Update Video Constraints

Update camera resolution while already streaming:

```jsx
meeting.self.updateVideoConstraints({
  width: { ideal: 1920 },
  height: { ideal: 1080 },
});
```

### Update Screenshare Constraints

Update screenshare resolution while already streaming:

```jsx
meeting.self.updateScreenshareConstraints({
  width: { ideal: 1920 },
  height: { ideal: 1080 },
});
```

Flutter SDK does not currently expose runtime constraint updates.

```json
{"@context":"https://schema.org","@type":"WebPage","@id":"https://edgetunnel-b2h.pages.dev/realtime/realtimekit/core/local-participant/#page","headline":"Local Participant · Cloudflare Realtime docs","description":"Manage local user media devices, audio, video, and screenshare in RealtimeKit meetings.","url":"https://edgetunnel-b2h.pages.dev/realtime/realtimekit/core/local-participant/","inLanguage":"en","image":"https://edgetunnel-b2h.pages.dev/dev-products-preview.png","dateModified":"2026-07-09","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://edgetunnel-b2h.pages.dev/#website","name":"Cloudflare Docs","url":"https://edgetunnel-b2h.pages.dev/"}}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/realtime/","name":"Realtime"}},{"@type":"ListItem","position":3,"item":{"@id":"/realtime/realtimekit/","name":"RealtimeKit"}},{"@type":"ListItem","position":4,"item":{"@id":"/realtime/realtimekit/core/","name":"Build using Core SDK"}},{"@type":"ListItem","position":5,"item":{"@id":"/realtime/realtimekit/core/local-participant/","name":"Local Participant"}}]}
```
