From 1ff6a0c5331d77941e21f4a42ea42355f14e2265 Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Mon, 28 Oct 2019 15:02:00 -0600 Subject: [PATCH 01/21] Add Participant, publication, and Track components and hooks --- src/App.tsx | 11 ++----- src/components/Participant/Participant.tsx | 23 +++++++++++++ .../ParticipantStrip/ParticipantStrip.tsx | 18 +++++++++++ src/components/Publication/Publication.tsx | 22 +++++++++++++ src/components/Room/Room.tsx | 10 ++++++ src/hooks/useParticipants/useParticipants.tsx | 27 ++++++++++++++++ src/hooks/usePublications/usePublications.tsx | 32 +++++++++++++++++++ src/hooks/useTrack/useTrack.tsx | 20 ++++++++++++ 8 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 src/components/Participant/Participant.tsx create mode 100644 src/components/ParticipantStrip/ParticipantStrip.tsx create mode 100644 src/components/Publication/Publication.tsx create mode 100644 src/components/Room/Room.tsx create mode 100644 src/hooks/useParticipants/useParticipants.tsx create mode 100644 src/hooks/usePublications/usePublications.tsx create mode 100644 src/hooks/useTrack/useTrack.tsx diff --git a/src/App.tsx b/src/App.tsx index d72b86638..dab06c318 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,9 +3,9 @@ import { styled } from '@material-ui/core/styles'; import LocalVideoPreview from './components/LocalVideoPreview/LocalVideoPreview'; import Menu from './components/Menu/Menu'; +import Room from './components/Room/Room'; import useRoomState from './hooks/useRoomState/useRoomState'; -import { useVideoContext } from './hooks/context'; const Container = styled('div')({ display: 'flex', @@ -17,19 +17,14 @@ const Main = styled('main')({ height: '100%', }); -export default function Room() { +export default function App() { const roomState = useRoomState(); - const { room } = useVideoContext(); return (
- {roomState === 'disconnected' ? ( - - ) : ( -

You have joined room {room.name}

- )} + {roomState === 'disconnected' ? : }
); diff --git a/src/components/Participant/Participant.tsx b/src/components/Participant/Participant.tsx new file mode 100644 index 000000000..4e537dae3 --- /dev/null +++ b/src/components/Participant/Participant.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import Publication from '../Publication/Publication'; +import usePublications from '../../hooks/usePublications/usePublications'; +import { LocalParticipant, RemoteParticipant } from 'twilio-video'; + +interface ParticipantProps { + participant: LocalParticipant | RemoteParticipant; +} + +export default function Participant({ participant }: ParticipantProps) { + const publications = usePublications(participant); + return ( + <> + {publications.map(publication => ( + + ))} + + ); +} diff --git a/src/components/ParticipantStrip/ParticipantStrip.tsx b/src/components/ParticipantStrip/ParticipantStrip.tsx new file mode 100644 index 000000000..e367a558b --- /dev/null +++ b/src/components/ParticipantStrip/ParticipantStrip.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useVideoContext } from '../../hooks/context'; +import useParticipants from '../../hooks/useParticipants/useParticipants'; +import Participant from '../Participant/Participant'; + +export default function ParticipantStrip() { + const { room } = useVideoContext(); + const participants = useParticipants(); + + return ( +
+ + {participants.map(participant => ( + + ))} +
+ ); +} diff --git a/src/components/Publication/Publication.tsx b/src/components/Publication/Publication.tsx new file mode 100644 index 000000000..c8e54ebde --- /dev/null +++ b/src/components/Publication/Publication.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import useTrack from '../../hooks/useTrack/useTrack'; +import VideoTrack from '../VideoTrack/VideoTrack'; +import { + Participant, + VideoTrack as IVideoTrack, + LocalTrackPublication, + RemoteTrackPublication, +} from 'twilio-video'; + +interface PublicationProps { + publication: LocalTrackPublication | RemoteTrackPublication; + participant: Participant; +} + +export default function Publication({ publication }: PublicationProps) { + const track = useTrack(publication); + if (track === null) return null; + return track.name === 'camera' ? ( + + ) : null; +} diff --git a/src/components/Room/Room.tsx b/src/components/Room/Room.tsx new file mode 100644 index 000000000..a53de9ed2 --- /dev/null +++ b/src/components/Room/Room.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ParticipantStrip from '../ParticipantStrip/ParticipantStrip'; + +export default function Room() { + return ( +
+ +
+ ); +} diff --git a/src/hooks/useParticipants/useParticipants.tsx b/src/hooks/useParticipants/useParticipants.tsx new file mode 100644 index 000000000..56cb12465 --- /dev/null +++ b/src/hooks/useParticipants/useParticipants.tsx @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react'; +import { useVideoContext } from '../context'; +import { RemoteParticipant } from 'twilio-video'; + +export default function useParticipants() { + const { room } = useVideoContext(); + const [participants, setParticipants] = useState( + Array.from(room.participants.values()) + ); + + useEffect(() => { + const participantConnected = (participant: RemoteParticipant) => + setParticipants(participants => [...participants, participant]); + const participantDisconnected = (participant: RemoteParticipant) => + setParticipants(participants => + participants.filter(p => p === participant) + ); + room.on('participantConnected', participantConnected); + room.on('participantDisconnected', participantDisconnected); + return () => { + room.off('participantConnected', participantConnected); + room.off('participantDisconnected', participantDisconnected); + }; + }, [room, setParticipants]); + + return participants; +} diff --git a/src/hooks/usePublications/usePublications.tsx b/src/hooks/usePublications/usePublications.tsx new file mode 100644 index 000000000..243263eff --- /dev/null +++ b/src/hooks/usePublications/usePublications.tsx @@ -0,0 +1,32 @@ +import { useEffect, useState } from 'react'; +import { + Participant, + LocalTrackPublication, + RemoteTrackPublication, +} from 'twilio-video'; + +type TrackPublication = LocalTrackPublication | RemoteTrackPublication; + +export default function usePublications(participant: Participant) { + const [publications, setPublications] = useState( + Array.from(participant.tracks.values()) as TrackPublication[] + ); + + useEffect(() => { + const publicationAdded = (publication: TrackPublication) => + setPublications(publications => [...publications, publication]); + const publicationRemoved = (publication: TrackPublication) => + setPublications(publications => + publications.filter(p => p !== publication) + ); + + participant.on('trackPublished', publicationAdded); + participant.on('trackRemoved', publicationRemoved); + return () => { + participant.off('trackPublished', publicationAdded); + participant.off('trackRemoved', publicationRemoved); + }; + }, [participant, setPublications]); + + return publications; +} diff --git a/src/hooks/useTrack/useTrack.tsx b/src/hooks/useTrack/useTrack.tsx new file mode 100644 index 000000000..ab0375da0 --- /dev/null +++ b/src/hooks/useTrack/useTrack.tsx @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; +import { LocalTrackPublication, RemoteTrackPublication } from 'twilio-video'; + +export default function useTrack( + publication: LocalTrackPublication | RemoteTrackPublication +) { + const [track, setTrack] = useState(publication.track); + + useEffect(() => { + const removeTrack = () => setTrack(null); + publication.on('subscribed', setTrack); + publication.on('unsubscribed', removeTrack); + return () => { + publication.off('subscribed', setTrack); + publication.off('unsubscribed', removeTrack); + }; + }, [publication]); + + return track; +} From 11e783c858e7d2714127923b3bacd4281eb7ffd9 Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Mon, 28 Oct 2019 15:15:06 -0600 Subject: [PATCH 02/21] Apply basic styles to ParticipantStrip --- src/components/Participant/Participant.tsx | 7 ++++++- .../ParticipantStrip/ParticipantStrip.tsx | 16 +++++++++++++--- src/components/Publication/Publication.tsx | 8 ++++++-- src/components/Room/Room.tsx | 10 ++++++++-- src/components/VideoTrack/VideoTrack.tsx | 2 +- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/components/Participant/Participant.tsx b/src/components/Participant/Participant.tsx index 4e537dae3..45f7025f0 100644 --- a/src/components/Participant/Participant.tsx +++ b/src/components/Participant/Participant.tsx @@ -5,9 +5,13 @@ import { LocalParticipant, RemoteParticipant } from 'twilio-video'; interface ParticipantProps { participant: LocalParticipant | RemoteParticipant; + isLocal?: boolean; } -export default function Participant({ participant }: ParticipantProps) { +export default function Participant({ + participant, + isLocal, +}: ParticipantProps) { const publications = usePublications(participant); return ( <> @@ -16,6 +20,7 @@ export default function Participant({ participant }: ParticipantProps) { key={publication.trackSid} publication={publication} participant={participant} + isLocal={isLocal} /> ))} diff --git a/src/components/ParticipantStrip/ParticipantStrip.tsx b/src/components/ParticipantStrip/ParticipantStrip.tsx index e367a558b..78014829e 100644 --- a/src/components/ParticipantStrip/ParticipantStrip.tsx +++ b/src/components/ParticipantStrip/ParticipantStrip.tsx @@ -2,17 +2,27 @@ import React from 'react'; import { useVideoContext } from '../../hooks/context'; import useParticipants from '../../hooks/useParticipants/useParticipants'; import Participant from '../Participant/Participant'; +import { styled } from '@material-ui/core/styles'; + +const Container = styled('aside')({ + position: 'absolute', + top: 0, + bottom: 0, + right: '85%', + left: 0, + padding: '0.5em', +}); export default function ParticipantStrip() { const { room } = useVideoContext(); const participants = useParticipants(); return ( -
- + + {participants.map(participant => ( ))} -
+ ); } diff --git a/src/components/Publication/Publication.tsx b/src/components/Publication/Publication.tsx index c8e54ebde..e3e5eb3ad 100644 --- a/src/components/Publication/Publication.tsx +++ b/src/components/Publication/Publication.tsx @@ -11,12 +11,16 @@ import { interface PublicationProps { publication: LocalTrackPublication | RemoteTrackPublication; participant: Participant; + isLocal?: boolean; } -export default function Publication({ publication }: PublicationProps) { +export default function Publication({ + publication, + isLocal, +}: PublicationProps) { const track = useTrack(publication); if (track === null) return null; return track.name === 'camera' ? ( - + ) : null; } diff --git a/src/components/Room/Room.tsx b/src/components/Room/Room.tsx index a53de9ed2..b21727982 100644 --- a/src/components/Room/Room.tsx +++ b/src/components/Room/Room.tsx @@ -1,10 +1,16 @@ import React from 'react'; import ParticipantStrip from '../ParticipantStrip/ParticipantStrip'; +import { styled } from '@material-ui/core/styles'; + +const Container = styled('div')({ + position: 'relative', + height: '100%', +}); export default function Room() { return ( -
+ -
+ ); } diff --git a/src/components/VideoTrack/VideoTrack.tsx b/src/components/VideoTrack/VideoTrack.tsx index 587f3fed7..dde489278 100644 --- a/src/components/VideoTrack/VideoTrack.tsx +++ b/src/components/VideoTrack/VideoTrack.tsx @@ -4,7 +4,7 @@ import { styled } from '@material-ui/core/styles'; const Video = styled('video')({ width: '100%', - height: '100%', + maxHeight: '100%', objectFit: 'contain', }); From a977ca426f410cb340e5d73c52fb9f00c33deef6 Mon Sep 17 00:00:00 2001 From: Tim Mendoza Date: Mon, 28 Oct 2019 17:05:35 -0600 Subject: [PATCH 03/21] Add AudioTrack component --- src/components/AudioTrack/AudioTrack.tsx | 20 ++++++++++++++++++++ src/components/Publication/Publication.tsx | 15 ++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 src/components/AudioTrack/AudioTrack.tsx diff --git a/src/components/AudioTrack/AudioTrack.tsx b/src/components/AudioTrack/AudioTrack.tsx new file mode 100644 index 000000000..b16542e1b --- /dev/null +++ b/src/components/AudioTrack/AudioTrack.tsx @@ -0,0 +1,20 @@ +import React, { useRef, useEffect } from 'react'; +import { AudioTrack as IAudioTrack } from 'twilio-video'; + +interface AudioTrackProps { + track: IAudioTrack; +} + +export default function AudioTrack({ track }: AudioTrackProps) { + const ref = useRef(null!); + + useEffect(() => { + const el = ref.current; + track.attach(el); + return () => { + track.detach(el); + }; + }, [track]); + + return