feat: 声网插件自由控制媒体流推送

并优化了自我画面的展示
pull/64/head
moonrailgun 2 years ago
parent 0cfa409caf
commit 356e7edd58

@ -1,45 +1,46 @@
import { IconBtn } from '@capital/component'; import { IconBtn } from '@capital/component';
import React, { useState } from 'react'; import React from 'react';
import { useClient, useMicrophoneAndCameraTracks } from './client'; import { useClient, useMicrophoneAndCameraTracks } from './client';
import { useMeetingStore } from './store'; import { useMeetingStore } from './store';
import { getClientLocalTrack } from './utils';
export const Controls: React.FC<{ export const Controls: React.FC<{
onClose: () => void; onClose: () => void;
}> = React.memo((props) => { }> = React.memo((props) => {
const client = useClient(); const client = useClient();
const [trackState, setTrackState] = useState({ video: false, audio: false });
const { ready, tracks } = useMicrophoneAndCameraTracks(); const { ready, tracks } = useMicrophoneAndCameraTracks();
const mediaPerm = useMeetingStore((state) => state.mediaPerm);
const mute = async (type: 'audio' | 'video') => { const mute = async (type: 'audio' | 'video') => {
if (type === 'audio') { if (type === 'audio') {
if (trackState.audio === true) { if (mediaPerm.audio === true) {
// await tracks[0].setEnabled(false); const track = getClientLocalTrack(client, 'audio');
await client.unpublish(tracks[0]); if (track) {
await client.unpublish(track);
}
} else { } else {
// await tracks[0].setEnabled(true);
await client.publish(tracks[0]); await client.publish(tracks[0]);
} }
setTrackState((ps) => {
return { ...ps, audio: !ps.audio }; useMeetingStore.getState().setMediaPerm({ audio: !mediaPerm.audio });
});
} else if (type === 'video') { } else if (type === 'video') {
if (trackState.video === true) { if (mediaPerm.video === true) {
// await tracks[1].setEnabled(false); const track = getClientLocalTrack(client, 'video');
await client.unpublish(tracks[1]); if (track) {
await client.unpublish(track);
}
} else { } else {
// await tracks[1].setEnabled(true);
await client.publish(tracks[1]); await client.publish(tracks[1]);
} }
setTrackState((ps) => {
return { ...ps, video: !ps.video }; useMeetingStore.getState().setMediaPerm({ video: !mediaPerm.video });
});
} }
}; };
const leaveChannel = async () => { const leaveChannel = async () => {
await client.leave(); await client.leave();
client.removeAllListeners(); client.removeAllListeners();
useMeetingStore.getState().clearUser(); useMeetingStore.getState().reset();
// we close the tracks to perform cleanup // we close the tracks to perform cleanup
tracks[0].close(); tracks[0].close();
tracks[1].close(); tracks[1].close();
@ -49,16 +50,16 @@ export const Controls: React.FC<{
return ( return (
<div className="controller"> <div className="controller">
<IconBtn <IconBtn
icon={trackState.video ? 'mdi:video' : 'mdi:video-off'} icon={mediaPerm.video ? 'mdi:video' : 'mdi:video-off'}
title={trackState.video ? '关闭摄像头' : '开启摄像头'} title={mediaPerm.video ? '关闭摄像头' : '开启摄像头'}
disabled={!ready} disabled={!ready}
size="large" size="large"
onClick={() => mute('video')} onClick={() => mute('video')}
/> />
<IconBtn <IconBtn
icon={trackState.audio ? 'mdi:microphone' : 'mdi:microphone-off'} icon={mediaPerm.audio ? 'mdi:microphone' : 'mdi:microphone-off'}
title={trackState.audio ? '关闭麦克风' : '开启麦克风'} title={mediaPerm.audio ? '关闭麦克风' : '开启麦克风'}
disabled={!ready} disabled={!ready}
size="large" size="large"
onClick={() => mute('audio')} onClick={() => mute('audio')}

@ -2,6 +2,8 @@ import { UserName } from '@capital/component';
import { AgoraVideoPlayer, IAgoraRTCRemoteUser } from 'agora-rtc-react'; import { AgoraVideoPlayer, IAgoraRTCRemoteUser } from 'agora-rtc-react';
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { useClient, useMicrophoneAndCameraTracks } from './client';
import { useMeetingStore } from './store';
const Root = styled.div` const Root = styled.div`
width: 95%; width: 95%;
@ -12,6 +14,7 @@ const Root = styled.div`
aspect-ratio: 16/9; aspect-ratio: 16/9;
justify-self: center; justify-self: center;
align-self: center; align-self: center;
overflow: hidden;
.player { .player {
width: 100%; width: 100%;
@ -42,3 +45,20 @@ export const VideoView: React.FC<{
); );
}; };
VideoView.displayName = 'VideoView'; VideoView.displayName = 'VideoView';
export const OwnVideoView: React.FC<{}> = React.memo(() => {
const { ready, tracks } = useMicrophoneAndCameraTracks();
const client = useClient();
const mediaPerm = useMeetingStore((state) => state.mediaPerm);
return (
<Root>
{ready && mediaPerm.video && (
<AgoraVideoPlayer className="player" videoTrack={tracks[1]} />
)}
<UserName className="name" userId={String(client.uid)} />
</Root>
);
});
OwnVideoView.displayName = 'OwnVideoView';

@ -1,26 +1,20 @@
import { AgoraVideoPlayer } from 'agora-rtc-react';
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { useMicrophoneAndCameraTracks } from './client';
import { useMeetingStore } from './store'; import { useMeetingStore } from './store';
import { VideoView } from './VideoView'; import { OwnVideoView, VideoView } from './VideoView';
const Root = styled.div` const Root = styled.div`
height: 70vh; height: 70vh;
/* align-self: flex-start; */
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(440px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(440px, 1fr));
`; `;
export const Videos: React.FC = React.memo(() => { export const Videos: React.FC = React.memo(() => {
const users = useMeetingStore((state) => state.users); const users = useMeetingStore((state) => state.users);
const { ready, tracks } = useMicrophoneAndCameraTracks();
return ( return (
<Root> <Root>
{/* AgoraVideoPlayer component takes in the video track to render the stream, <OwnVideoView />
you can pass in other props that get passed to the rendered div */}
{ready && <AgoraVideoPlayer className="vid" videoTrack={tracks[1]} />}
{users.length > 0 && {users.length > 0 &&
users.map((user) => { users.map((user) => {

@ -1,22 +1,34 @@
import type { IAgoraRTCRemoteUser } from 'agora-rtc-react'; import type { IAgoraRTCRemoteUser } from 'agora-rtc-react';
import create from 'zustand'; import create from 'zustand';
interface MediaPerm {
video: boolean;
audio: boolean;
}
interface MeetingState { interface MeetingState {
/** /**
* *
*/ */
users: IAgoraRTCRemoteUser[]; users: IAgoraRTCRemoteUser[];
/**
*
*/
mediaPerm: MediaPerm;
appendUser: (user: IAgoraRTCRemoteUser) => void; appendUser: (user: IAgoraRTCRemoteUser) => void;
removeUser: (user: IAgoraRTCRemoteUser) => void; removeUser: (user: IAgoraRTCRemoteUser) => void;
clearUser: () => void;
/** /**
* *
*/ */
updateUserInfo: (user: IAgoraRTCRemoteUser) => void; updateUserInfo: (user: IAgoraRTCRemoteUser) => void;
setMediaPerm: (perm: Partial<MediaPerm>) => void;
reset: () => void;
} }
export const useMeetingStore = create<MeetingState>((set) => ({ export const useMeetingStore = create<MeetingState>((set) => ({
users: [], users: [],
mediaPerm: { video: false, audio: false },
appendUser: (user: IAgoraRTCRemoteUser) => { appendUser: (user: IAgoraRTCRemoteUser) => {
set((state) => ({ set((state) => ({
users: [...state.users, user], users: [...state.users, user],
@ -29,9 +41,6 @@ export const useMeetingStore = create<MeetingState>((set) => ({
}; };
}); });
}, },
clearUser: () => {
set({ users: [] });
},
updateUserInfo: (user: IAgoraRTCRemoteUser) => { updateUserInfo: (user: IAgoraRTCRemoteUser) => {
set((state) => { set((state) => {
const users = [...state.users]; const users = [...state.users];
@ -47,4 +56,19 @@ export const useMeetingStore = create<MeetingState>((set) => ({
}; };
}); });
}, },
setMediaPerm: (perm: Partial<MediaPerm>) => {
set((state) => ({
mediaPerm: {
...state.mediaPerm,
...perm,
},
}));
},
reset: () => {
set({
users: [],
mediaPerm: { video: false, audio: false },
});
},
})); }));

@ -0,0 +1,15 @@
import type { IAgoraRTCClient, ILocalTrack } from 'agora-rtc-react';
/**
*
*/
export function getClientLocalTrack(
client: IAgoraRTCClient,
trackMediaType: 'audio' | 'video'
): ILocalTrack | null {
return (
client.localTracks.find(
(track) => track.trackMediaType === trackMediaType
) ?? null
);
}
Loading…
Cancel
Save