Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/components/Participant/Participant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ import { LocalParticipant, RemoteParticipant } from 'twilio-video';

interface ParticipantProps {
participant: LocalParticipant | RemoteParticipant;
disableAudio?: boolean;
}

export default function Participant({ participant }: ParticipantProps) {
export default function Participant({
participant,
disableAudio,
}: ParticipantProps) {
return (
<ParticipantInfo participant={participant}>
<ParticipantTracks participant={participant} />
<ParticipantTracks
participant={participant}
disableAudio={disableAudio}
/>
</ParticipantInfo>
);
}
6 changes: 3 additions & 3 deletions src/components/ParticipantStrip/ParticipantStrip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { styled } from '@material-ui/core/styles';
import useParticipants from '../../hooks/useParticipants/useParticipants';
import { useVideoContext } from '../../hooks/context';

const Container = styled('aside')({
const Container = styled('aside')(({ theme }) => ({
position: 'absolute',
top: 0,
bottom: 0,
right: '85%',
right: `calc(100% - ${theme.sidebarPosition})`,
left: 0,
padding: '0.5em',
});
}));

export default function ParticipantStrip() {
const { room } = useVideoContext();
Expand Down
3 changes: 3 additions & 0 deletions src/components/ParticipantTracks/ParticipantTracks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { useVideoContext } from '../../hooks/context';

interface ParticipantTracksProps {
participant: LocalParticipant | RemoteParticipant;
disableAudio?: boolean;
}

export default function ParticipantTracks({
participant,
disableAudio,
}: ParticipantTracksProps) {
const { room } = useVideoContext();
const publications = usePublications(participant);
Expand All @@ -23,6 +25,7 @@ export default function ParticipantTracks({
publication={publication}
participant={participant}
isLocal={isLocal}
disableAudio={disableAudio}
/>
))}
</>
Expand Down
14 changes: 14 additions & 0 deletions src/components/Publication/Publication.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ describe('the Publication component', () => {
expect(wrapper.find('AudioTrack').length).toBe(1);
});

it('should render null when the track has name "microphone" and disableAudio is true', () => {
mockUseTrack.mockImplementation(() => ({ name: 'microphone' }));
const wrapper = shallow(
<Publication
isLocal
publication={'mockPublication' as any}
participant={'mockParticipant' as any}
disableAudio={true}
/>
);
expect(useTrack).toHaveBeenCalledWith('mockPublication');
expect(wrapper.find('AudioTrack').length).toBe(0);
});

it('should render null when there is no track', () => {
mockUseTrack.mockImplementation(() => null);
const wrapper = shallow(
Expand Down
4 changes: 3 additions & 1 deletion src/components/Publication/Publication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ interface PublicationProps {
publication: LocalTrackPublication | RemoteTrackPublication;
participant: Participant;
isLocal: boolean;
disableAudio?: boolean;
}

export default function Publication({
publication,
isLocal,
disableAudio,
}: PublicationProps) {
const track = useTrack(publication);

Expand All @@ -28,7 +30,7 @@ export default function Publication({
case 'camera':
return <VideoTrack track={track as IVideoTrack} isLocal={isLocal} />;
case 'microphone':
return <AudioTrack track={track as IAudioTrack} />;
return disableAudio ? null : <AudioTrack track={track as IAudioTrack} />;
default:
return null;
}
Expand Down
17 changes: 17 additions & 0 deletions src/components/Room/Room.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
import React from 'react';
import ParticipantStrip from '../ParticipantStrip/ParticipantStrip';
import { styled } from '@material-ui/core/styles';
import useMainSpeaker from '../../hooks/useMainSpeaker/useMainSpeaker';
import Participant from '../Participant/Participant';

const Container = styled('div')({
position: 'relative',
height: '100%',
});

const MainParticipantContainer = styled('div')(({ theme }) => ({
position: 'absolute',
left: theme.sidebarPosition,
right: 0,
top: 0,
bottom: 0,
'& > div': {
height: '100%',
},
}));

export default function Room() {
const mainParticipant = useMainSpeaker();
return (
<Container>
<ParticipantStrip />
<MainParticipantContainer>
<Participant participant={mainParticipant} disableAudio />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is audio disabled by default in VBT?

Copy link
Contributor Author

@timmydoza timmydoza Oct 31, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disableAudio prevents an <audio> element from being rendered for this participant. I'm doing this here because this participant's <audio> element is already rendered in the <ParticipantStrip /> component.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Can you add a comment on this line and put what you just said?

</MainParticipantContainer>
</Container>
);
}
32 changes: 32 additions & 0 deletions src/hooks/useDominantSpeaker/useDominantSpeaker.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { act, renderHook } from '@testing-library/react-hooks';
import EventEmitter from 'events';
import useDominantSpeaker from './useDominantSpeaker';
import { useVideoContext } from '../context';

jest.mock('../context');
const mockUseVideoContext = useVideoContext as jest.Mock<any>;

describe('the useDominantSpeaker hook', () => {
const mockRoom: any = new EventEmitter();
mockRoom.dominantSpeaker = 'mockDominantSpeaker';
mockUseVideoContext.mockImplementation(() => ({ room: mockRoom }));

it('should return room.dominantSpeaker by default', () => {
const { result } = renderHook(useDominantSpeaker);
expect(result.current).toBe('mockDominantSpeaker');
});

it('should return respond to "dominantSpeakerChanged" events', async () => {
const { result } = renderHook(useDominantSpeaker);
act(() => {
mockRoom.emit('dominantSpeakerChanged', 'newDominantSpeaker');
});
expect(result.current).toBe('newDominantSpeaker');
});

it('should clean up listeners on unmount', () => {
const { unmount } = renderHook(useDominantSpeaker);
unmount();
expect(mockRoom.listenerCount('dominantSpeakerChanged')).toBe(0);
});
});
16 changes: 16 additions & 0 deletions src/hooks/useDominantSpeaker/useDominantSpeaker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useEffect, useState } from 'react';
import { useVideoContext } from '../context';

export default function useDominantSpeaker() {
const { room } = useVideoContext();
const [dominantSpeaker, setDominantSpeaker] = useState(room.dominantSpeaker);

useEffect(() => {
room.on('dominantSpeakerChanged', setDominantSpeaker);
return () => {
room.off('dominantSpeakerChanged', setDominantSpeaker);
};
}, [room]);

return dominantSpeaker;
}
42 changes: 42 additions & 0 deletions src/hooks/useMainSpeaker/useMainSpeaker.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import useMainSpeaker from './useMainSpeaker';
import { renderHook } from '@testing-library/react-hooks';
import { useVideoContext } from '../context';
import { EventEmitter } from 'events';

jest.mock('../context');
const mockUseVideoContext = useVideoContext as jest.Mock<any>;

describe('the useMainSpeaker hook', () => {
it('should return the dominant speaker if it exists', () => {
const mockRoom: any = new EventEmitter();
mockRoom.dominantSpeaker = 'dominantSpeaker';
mockRoom.participants = new Map([[0, 'participant']]) as any;
mockRoom.localParticipant = 'localParticipant';
mockUseVideoContext.mockImplementation(() => ({ room: mockRoom }));
const { result } = renderHook(useMainSpeaker);
expect(result.current).toBe('dominantSpeaker');
});

it('should return the first remote participant if it exists', () => {
const mockRoom: any = new EventEmitter();
mockRoom.dominantSpeaker = null;
mockRoom.participants = new Map([
[0, 'participant'],
[1, 'secondParticipant'],
]) as any;
mockRoom.localParticipant = 'localParticipant';
mockUseVideoContext.mockImplementation(() => ({ room: mockRoom }));
const { result } = renderHook(useMainSpeaker);
expect(result.current).toBe('participant');
});

it('should return the local participant if it exists', () => {
const mockRoom: any = new EventEmitter();
mockRoom.dominantSpeaker = null;
mockRoom.participants = new Map() as any;
mockRoom.localParticipant = 'localParticipant';
mockUseVideoContext.mockImplementation(() => ({ room: mockRoom }));
const { result } = renderHook(useMainSpeaker);
expect(result.current).toBe('localParticipant');
});
});
11 changes: 11 additions & 0 deletions src/hooks/useMainSpeaker/useMainSpeaker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useVideoContext } from '../context';
import useDominantSpeaker from '../useDominantSpeaker/useDominantSpeaker';
import useParticipants from '../useParticipants/useParticipants';

export default function useMainSpeaker() {
const { room } = useVideoContext();
const participants = useParticipants();
const dominantSpeaker = useDominantSpeaker();

return dominantSpeaker || participants[0] || room.localParticipant;
}
12 changes: 12 additions & 0 deletions src/theme.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import { createMuiTheme } from '@material-ui/core';

declare module '@material-ui/core/styles/createMuiTheme' {
interface Theme {
sidebarPosition: string;
}

// allow configuration using `createMuiTheme`
interface ThemeOptions {
sidebarPosition?: string;
}
}

export default createMuiTheme({
palette: {
type: 'dark',
primary: {
main: '#cc2b33',
},
},
sidebarPosition: '15%',
});