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";