Skip to content

Commit

Permalink
refactor(rtc): add LocalUser
Browse files Browse the repository at this point in the history
  • Loading branch information
crimx committed Mar 28, 2023
1 parent ca4912b commit 12cb856
Show file tree
Hide file tree
Showing 12 changed files with 287 additions and 90 deletions.
26 changes: 26 additions & 0 deletions examples/multi-channel/src/CurrentUser.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<User uid={client.uid}>
<LocalUser cameraOn micOn audioTrack={localTracks[0]} videoTrack={localTracks[1]} />
</User>
);
}
36 changes: 1 addition & 35 deletions examples/multi-channel/src/RemoteUserList.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
19 changes: 4 additions & 15 deletions examples/multi-channel/src/RemoteUserList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -14,20 +13,10 @@ export function RemoteUserList({ users }: RemoteUserListProps) {
return (
<div className={styles.container}>
{users.map(user => (
<RemoteUserListItem key={user.uid} user={user} />
<User key={user.uid} uid={user.uid}>
<RemoteUser user={user} />
</User>
))}
</div>
);
}

function RemoteUserListItem({ user }: { user: IAgoraRTCRemoteUser }) {
const name = useMemo(() => fakeName(user.uid), [user.uid]);

return (
<div className={styles.user}>
<RemoteUser user={user} />
<div className={styles.mask}></div>
<label className={styles.label}>{name}</label>
</div>
);
}
2 changes: 2 additions & 0 deletions examples/multi-channel/src/RoomSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ 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();
return (
<div className={styles.item}>
<i className="i-mdi-chat-processing-outline text-5 color-zinc" />
<code className="bg-rose color-white px-2 rounded">{label}</code>
<CurrentUser />
<RemoteUserList users={remoteUsers} />
</div>
);
Expand Down
34 changes: 34 additions & 0 deletions examples/multi-channel/src/User.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
23 changes: 23 additions & 0 deletions examples/multi-channel/src/User.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.user}>
{children}
<div className={styles.mask}></div>
<label className={styles.label}>{name}</label>
</div>
);
}
11 changes: 5 additions & 6 deletions examples/multi-channel/src/stores/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ export const useAppStore = create<AppState>((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) {
if (localTracks && hostRoom.client.localTracks.length > 0) {
await hostRoom.client.unpublish(localTracks);
}
hostRoom.client.setClientRole("audience");
rooms = [...rooms, hostRoom];
hostRoom = null;
set({ hostRoom, rooms, localTracks });
set({ hostRoom, localTracks });
}
return;
}
Expand All @@ -53,7 +53,6 @@ export const useAppStore = create<AppState>((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) {
Expand All @@ -63,8 +62,8 @@ export const useAppStore = create<AppState>((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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<LocalMicrophoneAndCameraUserProps> = {
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<LocalMicrophoneAndCameraUserProps, keyof OverviewProps>;

export const Overview: StoryObj<OverviewArgs> = {
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 (
<AgoraRTCProvider client={client}>
<LocalMicrophoneAndCameraUser
audioTrack={audioTrack}
videoTrack={videoTrack}
micOn={micOn}
cameraOn={cameraOn}
{...args}
/>
</AgoraRTCProvider>
);
},
};
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement> {
/**
* 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<IMicrophoneAudioTrack>;
/**
* A camera video track which can be created by `createCameraVideoTrack()`.
*/
readonly videoTrack?: MaybePromiseOrNull<ICameraVideoTrack>;
/**
* 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 (
<div {...props} style={mergedStyle}>
<CameraVideoTrack
track={videoTrack}
play={playVideo}
deviceId={cameraDeviceId}
disabled={!cameraOn}
/>
<MicrophoneAudioTrack
track={audioTrack}
play={playAudio}
deviceId={micDeviceId}
disabled={!micOn}
/>
{cover && !cameraOn && <UserCover cover={cover} />}
<div style={FloatBoxStyle}>{children}</div>
</div>
);
}
Loading

0 comments on commit 12cb856

Please sign in to comment.