PeerGridItem.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import { Media, Peer, PeerControls, Video } from '@andyet/simplewebrtc';
  2. import VolumeOffIcon from 'material-icons-svg/components/baseline/VolumeOff';
  3. import VolumeUpIcon from 'material-icons-svg/components/baseline/VolumeUp';
  4. import React, { useContext } from 'react';
  5. import styled from 'styled-components';
  6. import HiddenPeers from '../contexts/HiddenPeers';
  7. import { TalkyButton } from '../styles/button';
  8. import AudioOnlyPeer from './AudioOnlyPeer';
  9. const MuteButton = styled(TalkyButton)({
  10. display: 'inline-block',
  11. justifySelf: 'flex-end',
  12. opacity: 0.3,
  13. backgroundColor: 'black',
  14. color: 'white',
  15. transition: 'opacity 200ms linear',
  16. marginBottom: '16px',
  17. marginLeft: '16px',
  18. ':hover': {
  19. backgroundColor: 'black',
  20. opacity: 0.7
  21. }
  22. });
  23. const DisplayName = styled.span({
  24. display: 'inline-block',
  25. backgroundColor: 'black',
  26. opacity: 0.3,
  27. color: 'white',
  28. marginTop: '16px',
  29. marginLeft: '16px',
  30. fontSize: '16px',
  31. padding: '2px 7px 2px 9px',
  32. borderRadius: '5px',
  33. transition: 'opacity 200ms linear',
  34. '&:hover': {
  35. cursor: 'pointer',
  36. opacity: 0.7
  37. }
  38. });
  39. const PictureInPictureContainer = styled.div({
  40. position: 'relative',
  41. display: 'flex',
  42. justifyContent: 'center',
  43. alignItems: 'center',
  44. '& video:first-of-type': {},
  45. '& video:last-of-type': {
  46. position: 'absolute',
  47. top: '16px',
  48. right: '16px',
  49. width: '100px'
  50. }
  51. });
  52. interface PeerGridItemMediaProps {
  53. media: Media[];
  54. }
  55. // PeerGridItemMedia renders a different visualization based on what media is
  56. // available from a peer. It will render video if the peer is sending video,
  57. // otherwise it renders an audio-only display.
  58. const PeerGridItemMedia: React.SFC<PeerGridItemMediaProps> = ({ media }) => {
  59. const videoStreams = media.filter(
  60. m => m.kind === 'video' && !m.remoteDisabled
  61. );
  62. const audioStreams = media.filter(m => m.kind === 'audio');
  63. if (videoStreams.length > 0) {
  64. // Choose last media as it is most likely the screenshare.
  65. const webcamStreams = videoStreams.filter(s => !s.screenCapture);
  66. const screenCaptureStreams = videoStreams.filter(s => s.screenCapture);
  67. if (videoStreams.length === 1) {
  68. return <Video media={videoStreams[0]} />;
  69. }
  70. if (screenCaptureStreams.length === 0) {
  71. return <Video media={webcamStreams[0]} />;
  72. }
  73. return (
  74. <PictureInPictureContainer>
  75. {/* Screenshare */}
  76. <Video media={screenCaptureStreams[0]} />
  77. {/* Camera */}
  78. <Video media={webcamStreams[0]} />
  79. </PictureInPictureContainer>
  80. );
  81. } else if (audioStreams.length > 0) {
  82. return <AudioOnlyPeer />;
  83. }
  84. return <div>No media</div>;
  85. };
  86. const Overlay = styled.div({
  87. position: 'absolute',
  88. top: 0,
  89. right: 0,
  90. bottom: 0,
  91. left: 0,
  92. zIndex: 100,
  93. display: 'flex',
  94. flexDirection: 'column'
  95. });
  96. const RttContainer = styled.div({
  97. flex: 1,
  98. display: 'flex',
  99. justifyContent: 'center',
  100. alignItems: 'flex-end',
  101. '& span': {
  102. backgroundColor: 'rgba(0, 0, 0, 0.3)',
  103. color: 'white',
  104. padding: '8px 16px',
  105. borderRadius: '4px',
  106. fontSize: '18px'
  107. }
  108. });
  109. const MuteIndicator = styled.span({
  110. textAlign: 'center',
  111. fontSize: '48px',
  112. opacity: 0.8
  113. });
  114. function allAudioIsUnmuted(media: Media[]): boolean {
  115. for (const m of media) {
  116. if (m.kind === 'audio' && m.remoteDisabled) {
  117. return false;
  118. }
  119. }
  120. return true;
  121. }
  122. interface PeerGridItemOverlayProps {
  123. peer: Peer;
  124. audioIsMuted: boolean;
  125. }
  126. const PeerGridItemOverlay: React.SFC<PeerGridItemOverlayProps> = ({
  127. peer,
  128. audioIsMuted
  129. }) => {
  130. const { togglePeer } = useContext(HiddenPeers);
  131. return (
  132. <Overlay>
  133. <div>
  134. <DisplayName onClick={() => togglePeer(peer.id)}>
  135. {`X ${peer.displayName}`}
  136. </DisplayName>
  137. </div>
  138. <RttContainer>{peer.rtt && <span>{peer.rtt}</span>}</RttContainer>
  139. <MuteIndicator>
  140. {peer.muted || audioIsMuted ? (
  141. <VolumeOffIcon fill={peer.speaking ? 'red' : 'white'} />
  142. ) : null}
  143. </MuteIndicator>
  144. <PeerControls
  145. peer={peer}
  146. render={({ isMuted, mute, unmute }) => (
  147. <div>
  148. <MuteButton onClick={() => (isMuted ? unmute() : mute())}>
  149. {isMuted ? (
  150. <>
  151. <VolumeOffIcon fill="white" />
  152. <span>Unmute</span>
  153. </>
  154. ) : (
  155. <>
  156. <VolumeUpIcon fill="white" />
  157. <span>Mute</span>
  158. </>
  159. )}
  160. </MuteButton>
  161. </div>
  162. )}
  163. />
  164. </Overlay>
  165. );
  166. };
  167. interface PeerGridItemProps {
  168. peer: Peer;
  169. media: Media[];
  170. }
  171. // PeerGridItem renders various controls over a peer's media.
  172. const PeerGridItem: React.SFC<PeerGridItemProps> = ({ peer, media }) => (
  173. <>
  174. <PeerGridItemOverlay peer={peer} audioIsMuted={!allAudioIsUnmuted(media)} />
  175. <PeerGridItemMedia media={media} />
  176. </>
  177. );
  178. export default PeerGridItem;