diff --git a/examples/multi-channel/src/CurrentUser.tsx b/examples/multi-channel/src/CurrentUser.tsx
new file mode 100644
index 00000000..24fdc09a
--- /dev/null
+++ b/examples/multi-channel/src/CurrentUser.tsx
@@ -0,0 +1,26 @@
+import { LocalUser, useIsConnected, useRTCClient } from "agora-rtc-react";
+import { useAppStore } from "./stores";
+import { User } from "./User";
+
+export function CurrentUser() {
+ const localTracks = useAppStore(state => state.localTracks);
+ const hostRoom = useAppStore(state => state.hostRoom);
+ const isConnected = useIsConnected();
+ const client = useRTCClient();
+
+ if (
+ !isConnected ||
+ !localTracks ||
+ !client.uid ||
+ !hostRoom ||
+ hostRoom.client.channelName !== client.channelName
+ ) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/examples/multi-channel/src/RemoteUserList.module.css b/examples/multi-channel/src/RemoteUserList.module.css
index 1347a0a1..e0355a6c 100644
--- a/examples/multi-channel/src/RemoteUserList.module.css
+++ b/examples/multi-channel/src/RemoteUserList.module.css
@@ -1,39 +1,5 @@
.container {
display: flex;
flex-wrap: nowrap;
-}
-
-.user {
- position: relative;
- width: 54px;
- height: 40px;
- overflow: hidden;
- border-radius: 5px;
-}
-
-.mask {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: linear-gradient(
- rgba(0, 0, 0, 0.1) 60%,
- rgba(0, 0, 0, 0.3) 80%,
- rgba(0, 0, 0, 0.8) 100%
- );
-}
-
-.label {
- position: absolute;
- z-index: 1;
- left: 2px;
- bottom: -2px;
- width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- word-break: keep-all;
- font-size: 12px;
- color: #fff;
+ gap: 10px;
}
diff --git a/examples/multi-channel/src/RemoteUserList.tsx b/examples/multi-channel/src/RemoteUserList.tsx
index c0f98001..80727312 100644
--- a/examples/multi-channel/src/RemoteUserList.tsx
+++ b/examples/multi-channel/src/RemoteUserList.tsx
@@ -3,8 +3,7 @@ import styles from "./RemoteUserList.module.css";
import type { IAgoraRTCRemoteUser } from "agora-rtc-sdk-ng";
import { RemoteUser } from "agora-rtc-react";
-import { useMemo } from "react";
-import { fakeName } from "./utils";
+import { User } from "./User";
export interface RemoteUserListProps {
users: IAgoraRTCRemoteUser[];
@@ -14,20 +13,10 @@ export function RemoteUserList({ users }: RemoteUserListProps) {
return (
{users.map(user => (
-
+
+
+
))}
);
}
-
-function RemoteUserListItem({ user }: { user: IAgoraRTCRemoteUser }) {
- const name = useMemo(() => fakeName(user.uid), [user.uid]);
-
- return (
-
- );
-}
diff --git a/examples/multi-channel/src/RoomSelector.tsx b/examples/multi-channel/src/RoomSelector.tsx
index 9a5fc425..35df5f87 100644
--- a/examples/multi-channel/src/RoomSelector.tsx
+++ b/examples/multi-channel/src/RoomSelector.tsx
@@ -8,6 +8,7 @@ import type { OptionProps } from "react-select";
import { RemoteUserList } from "./RemoteUserList";
import type { Room } from "./stores";
import { useAppStore } from "./stores";
+import { CurrentUser } from "./CurrentUser";
const Item = memo(function Item({ label }: { channel: string; label: string }) {
const remoteUsers = usePublishedRemoteUsers();
@@ -15,6 +16,7 @@ const Item = memo(function Item({ label }: { channel: string; label: string }) {
{label}
+
);
diff --git a/examples/multi-channel/src/User.module.css b/examples/multi-channel/src/User.module.css
new file mode 100644
index 00000000..65ad0fb0
--- /dev/null
+++ b/examples/multi-channel/src/User.module.css
@@ -0,0 +1,34 @@
+.user {
+ position: relative;
+ width: 54px;
+ height: 40px;
+ overflow: hidden;
+ border-radius: 5px;
+}
+
+.mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ rgba(0, 0, 0, 0.1) 60%,
+ rgba(0, 0, 0, 0.3) 80%,
+ rgba(0, 0, 0, 0.8) 100%
+ );
+}
+
+.label {
+ position: absolute;
+ z-index: 1;
+ left: 2px;
+ bottom: -2px;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ word-break: keep-all;
+ font-size: 12px;
+ color: #fff;
+}
diff --git a/examples/multi-channel/src/User.tsx b/examples/multi-channel/src/User.tsx
new file mode 100644
index 00000000..d839f295
--- /dev/null
+++ b/examples/multi-channel/src/User.tsx
@@ -0,0 +1,23 @@
+import styles from "./User.module.css";
+
+import type { UID } from "agora-rtc-sdk-ng";
+import type { PropsWithChildren } from "react";
+
+import { useMemo } from "react";
+import { fakeName } from "./utils";
+
+export interface UserProps extends PropsWithChildren {
+ uid: UID;
+}
+
+export function User({ uid, children }: UserProps) {
+ const name = useMemo(() => fakeName(uid), [uid]);
+
+ return (
+
+ {children}
+
+
+
+ );
+}
diff --git a/examples/multi-channel/src/stores/app.ts b/examples/multi-channel/src/stores/app.ts
index a2e68bba..63018271 100644
--- a/examples/multi-channel/src/stores/app.ts
+++ b/examples/multi-channel/src/stores/app.ts
@@ -30,7 +30,8 @@ export const useAppStore = create((set, get) => {
return { token, channel, client };
}),
selectChannel: async (channel?: string | null) => {
- let { hostRoom, rooms, localTracks } = get();
+ let { hostRoom, localTracks } = get();
+ const { rooms } = get();
if (!channel) {
if (hostRoom) {
@@ -38,9 +39,8 @@ export const useAppStore = create((set, get) => {
await hostRoom.client.unpublish(localTracks);
}
hostRoom.client.setClientRole("audience");
- rooms = [...rooms, hostRoom];
hostRoom = null;
- set({ hostRoom, rooms, localTracks });
+ set({ hostRoom, localTracks });
}
return;
}
@@ -53,7 +53,6 @@ export const useAppStore = create((set, get) => {
await hostRoom.client.unpublish(localTracks);
}
await hostRoom.client.setClientRole("audience");
- rooms = [...rooms, hostRoom];
}
hostRoom = rooms.find(room => room.client.channelName === channel);
if (hostRoom) {
@@ -63,8 +62,8 @@ export const useAppStore = create((set, get) => {
}
await hostRoom.client.publish(localTracks);
}
- rooms = rooms.filter(room => room.client.channelName !== channel);
- set({ localTracks, hostRoom, rooms });
+
+ set({ localTracks, hostRoom });
},
dispose: () => {
const { rooms, hostRoom } = get();
diff --git a/packages/agora-rtc-react/src/components/LocalMicrophoneAndCameraUser.stories.tsx b/packages/agora-rtc-react/src/components/LocalMicrophoneAndCameraUser.stories.tsx
new file mode 100644
index 00000000..5b494524
--- /dev/null
+++ b/packages/agora-rtc-react/src/components/LocalMicrophoneAndCameraUser.stories.tsx
@@ -0,0 +1,81 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import type { LocalMicrophoneAndCameraUserProps } from "./LocalMicrophoneAndCameraUser";
+
+import { action } from "@storybook/addon-actions";
+import { FakeRTCClient, FakeCameraVideoTrack, FakeMicrophoneAudioTrack } from "fake-agora-rtc";
+import { useEffect, useMemo, useState } from "react";
+import { AgoraRTCProvider } from "../hooks";
+import { LocalMicrophoneAndCameraUser } from "./LocalMicrophoneAndCameraUser";
+
+const meta: Meta = {
+ title: "User/LocalMicrophoneAndCameraUser",
+ component: LocalMicrophoneAndCameraUser,
+ tags: ["autodocs"],
+ parameters: {
+ backgrounds: { default: "light" },
+ },
+};
+
+export default meta;
+
+export interface OverviewProps {
+ micOn: boolean;
+ cameraOn: boolean;
+}
+
+type OverviewArgs = OverviewProps & Omit;
+
+export const Overview: StoryObj = {
+ args: {
+ micOn: false,
+ cameraOn: false,
+ playVideo: false,
+ playAudio: false,
+ cover: "http://placekitten.com/200/200",
+ style: {
+ width: 288,
+ height: 216,
+ },
+ },
+ render: function RenderLocalUser({ micOn, cameraOn, ...args }: OverviewArgs) {
+ const [client] = useState(() =>
+ FakeRTCClient.create({
+ publish: async () => {
+ action("IAgoraRTCClient.publish()")();
+ },
+ }),
+ );
+
+ const audioTrack = useMemo(() => {
+ return micOn ? FakeMicrophoneAudioTrack.create() : null;
+ }, [micOn]);
+
+ const videoTrack = useMemo(() => {
+ return cameraOn ? FakeCameraVideoTrack.create() : null;
+ }, [cameraOn]);
+
+ useEffect(() => {
+ if (client && audioTrack) {
+ client.publish(audioTrack);
+ }
+ }, [client, audioTrack]);
+
+ useEffect(() => {
+ if (client && videoTrack) {
+ client.publish(videoTrack);
+ }
+ }, [client, videoTrack]);
+
+ return (
+
+
+
+ );
+ },
+};
diff --git a/packages/agora-rtc-react/src/components/LocalMicrophoneAndCameraUser.tsx b/packages/agora-rtc-react/src/components/LocalMicrophoneAndCameraUser.tsx
new file mode 100644
index 00000000..8f70e83d
--- /dev/null
+++ b/packages/agora-rtc-react/src/components/LocalMicrophoneAndCameraUser.tsx
@@ -0,0 +1,96 @@
+import type { ICameraVideoTrack, IMicrophoneAudioTrack } from "agora-rtc-sdk-ng";
+import type { HTMLProps, ReactNode } from "react";
+import type { MaybePromiseOrNull } from "../utils";
+
+import { CameraVideoTrack } from "./CameraVideoTrack";
+import { MicrophoneAudioTrack } from "./MicrophoneAudioTrack";
+import { UserCover } from "./UserCover";
+import { FloatBoxStyle, useMergedStyle, VideoTrackWrapperStyle } from "./styles";
+
+export interface LocalMicrophoneAndCameraUserProps extends HTMLProps {
+ /**
+ * Whether to turn on the local user's microphone. Default false.
+ */
+ readonly micOn?: boolean;
+ /**
+ * Whether to turn on the local user's camera. Default false.
+ */
+ readonly cameraOn?: boolean;
+ /**
+ * A microphone audio track which can be created by `createMicrophoneAudioTrack()`.
+ */
+ readonly audioTrack?: MaybePromiseOrNull;
+ /**
+ * A camera video track which can be created by `createCameraVideoTrack()`.
+ */
+ readonly videoTrack?: MaybePromiseOrNull;
+ /**
+ * Whether to play the local user's audio track. Default follows `micOn`.
+ */
+ readonly playAudio?: boolean;
+ /**
+ * Whether to play the local user's video track. Default follows `cameraOn`.
+ */
+ readonly playVideo?: boolean;
+ /**
+ * Device ID, which can be retrieved by calling `getDevices()`.
+ */
+ readonly micDeviceId?: string;
+ /**
+ * Device ID, which can be retrieved by calling `getDevices()`.
+ */
+ readonly cameraDeviceId?: string;
+ /**
+ * The volume. The value ranges from 0 (mute) to 1000 (maximum). A value of 100 is the current volume.
+ */
+ readonly volume?: number;
+ /**
+ * Render cover image if playVideo is off.
+ */
+ readonly cover?: string;
+ /**
+ * Children is rendered on top of the video canvas.
+ */
+ readonly children?: ReactNode;
+}
+
+/**
+ * Play/Stop local user camera and microphone track.
+ */
+export function LocalMicrophoneAndCameraUser({
+ micOn,
+ cameraOn,
+ audioTrack,
+ videoTrack,
+ playAudio,
+ playVideo,
+ micDeviceId,
+ cameraDeviceId,
+ volume,
+ cover,
+ children,
+ style,
+ ...props
+}: LocalMicrophoneAndCameraUserProps) {
+ const mergedStyle = useMergedStyle(VideoTrackWrapperStyle, style);
+ playVideo = playVideo ?? !!cameraOn;
+ playAudio = playAudio ?? !!micOn;
+ return (
+
+
+
+ {cover && !cameraOn &&
}
+
{children}
+
+ );
+}
diff --git a/packages/agora-rtc-react/src/components/LocalUser.stories.tsx b/packages/agora-rtc-react/src/components/LocalUser.stories.tsx
index 12093529..38017587 100644
--- a/packages/agora-rtc-react/src/components/LocalUser.stories.tsx
+++ b/packages/agora-rtc-react/src/components/LocalUser.stories.tsx
@@ -1,15 +1,15 @@
import type { Meta, StoryObj } from "@storybook/react";
-import type { LocalMicrophoneAndCameraUserProps } from "./LocalUser";
+import type { LocalMicrophoneAndCameraUserProps } from "./LocalMicrophoneAndCameraUser";
import { action } from "@storybook/addon-actions";
import { FakeRTCClient, FakeCameraVideoTrack, FakeMicrophoneAudioTrack } from "fake-agora-rtc";
import { useEffect, useMemo, useState } from "react";
import { AgoraRTCProvider } from "../hooks";
-import { LocalMicrophoneAndCameraUser } from "./LocalUser";
+import { LocalUser } from "./LocalUser";
const meta: Meta = {
title: "User/LocalUser",
- component: LocalMicrophoneAndCameraUser,
+ component: LocalUser,
tags: ["autodocs"],
parameters: {
backgrounds: { default: "light" },
@@ -68,7 +68,7 @@ export const Overview: StoryObj = {
return (
- {
+export interface LocalUserProps extends HTMLProps {
/**
* Whether to turn on the local user's microphone. Default false.
*/
@@ -19,11 +19,11 @@ export interface LocalMicrophoneAndCameraUserProps extends HTMLProps;
+ readonly audioTrack?: MaybePromiseOrNull;
/**
* A camera video track which can be created by `createCameraVideoTrack()`.
*/
- readonly videoTrack?: MaybePromiseOrNull;
+ readonly videoTrack?: MaybePromiseOrNull;
/**
* Whether to play the local user's audio track. Default follows `micOn`.
*/
@@ -32,14 +32,6 @@ export interface LocalMicrophoneAndCameraUserProps extends HTMLProps
-
-
+
+
{cover && !cameraOn && }
{children}
diff --git a/packages/agora-rtc-react/src/components/index.ts b/packages/agora-rtc-react/src/components/index.ts
index 18736c9a..db7c949b 100644
--- a/packages/agora-rtc-react/src/components/index.ts
+++ b/packages/agora-rtc-react/src/components/index.ts
@@ -2,6 +2,7 @@ export * from "./LocalAudioTrack";
export * from "./LocalVideoTrack";
export * from "./MicrophoneAudioTrack";
export * from "./CameraVideoTrack";
+export * from "./LocalMicrophoneAndCameraUser";
export * from "./LocalUser";
export * from "./RemoteAudioTrack";
export * from "./RemoteVideoTrack";