diff --git a/src/components/Pages/Conference/Conference.tsx b/src/components/Pages/Conference/Conference.tsx index c6addb4..12cb3ed 100644 --- a/src/components/Pages/Conference/Conference.tsx +++ b/src/components/Pages/Conference/Conference.tsx @@ -1,6 +1,7 @@ import React from "react" import { Props } from "./Conference.types" -import { LiveKitRoom, VideoConference } from "@livekit/components-react" +import { LiveKitRoom } from "@livekit/components-react" +import { VideoConference } from "../../VideoConference/Videoconference" import "@livekit/components-styles" import "./Conference.css" diff --git a/src/components/VideoConference/ParticipantTile.tsx b/src/components/VideoConference/ParticipantTile.tsx new file mode 100644 index 0000000..299121f --- /dev/null +++ b/src/components/VideoConference/ParticipantTile.tsx @@ -0,0 +1,156 @@ +import * as React from "react" +import type { Participant, TrackPublication } from "livekit-client" +import { Track } from "livekit-client" +import type { ParticipantClickEvent, TrackReferenceOrPlaceholder } from "@livekit/components-core" +import { isParticipantSourcePinned } from "@livekit/components-core" +import { + AudioTrack, + ConnectionQualityIndicator, + FocusToggle, + ParticipantContext, + ParticipantName, + TrackMutedIndicator, + VideoTrack, + useEnsureParticipant, + useMaybeLayoutContext, + useMaybeParticipantContext, + useMaybeTrackContext, + useParticipantTile, +} from "@livekit/components-react" +import Profile from "decentraland-dapps/dist/containers/Profile" + +/** @public */ +export function ParticipantContextIfNeeded( + props: React.PropsWithChildren<{ + participant?: Participant + }> +) { + const hasContext = !!useMaybeParticipantContext() + return props.participant && !hasContext ? ( + {props.children} + ) : ( + <>{props.children}> + ) +} + +/** @public */ +export interface ParticipantTileProps extends React.HTMLAttributes { + disableSpeakingIndicator?: boolean + participant?: Participant + source?: Track.Source + publication?: TrackPublication + onParticipantClick?: (event: ParticipantClickEvent) => void + imageSize?: "normal" | "large" | "huge" | "massive" +} + +/** + * The ParticipantTile component is the base utility wrapper for displaying a visual representation of a participant. + * This component can be used as a child of the `TrackLoop` component or by spreading a track reference as properties. + * + * @example + * ```tsx + * + * + * + * ``` + * @public + */ +export function ParticipantTile({ + participant, + children, + source = Track.Source.Camera, + onParticipantClick, + publication, + disableSpeakingIndicator, + imageSize, + ...htmlProps +}: ParticipantTileProps) { + const p = useEnsureParticipant(participant) + const trackRef: TrackReferenceOrPlaceholder = useMaybeTrackContext() ?? { + participant: p, + source, + publication, + } + + const { elementProps } = useParticipantTile({ + participant: trackRef.participant, + htmlProps, + source: trackRef.source, + publication: trackRef.publication, + disableSpeakingIndicator, + onParticipantClick, + }) + + const layoutContext = useMaybeLayoutContext() + + const handleSubscribe = React.useCallback( + (subscribed: boolean) => { + if ( + trackRef.source && + !subscribed && + layoutContext && + layoutContext.pin.dispatch && + isParticipantSourcePinned(trackRef.participant, trackRef.source, layoutContext.pin.state) + ) { + layoutContext.pin.dispatch({ msg: "clear_pin" }) + } + }, + [trackRef.participant, layoutContext, trackRef.source] + ) + + const participantWithProfile: Participant = React.useMemo( + () => ({ + ...trackRef.participant, + name: "Edita me", + }), + [trackRef.participant] + ) as Participant + + return ( + + + {children ?? ( + <> + {trackRef.publication?.kind === "video" || + trackRef.source === Track.Source.Camera || + trackRef.source === Track.Source.ScreenShare ? ( + + ) : ( + + )} + + + + + + {trackRef.source === Track.Source.Camera ? ( + <> + + + > + ) : ( + <> + {/* */} + 's screen + > + )} + + + + > + )} + + + + ) +} diff --git a/src/components/VideoConference/Videoconference.tsx b/src/components/VideoConference/Videoconference.tsx new file mode 100644 index 0000000..6663cd1 --- /dev/null +++ b/src/components/VideoConference/Videoconference.tsx @@ -0,0 +1,129 @@ +import * as React from "react" +import type { WidgetState } from "@livekit/components-core" +import { isEqualTrackRef, isTrackReference, log, isWeb } from "@livekit/components-core" +import { RoomEvent, Track } from "livekit-client" +import type { TrackReferenceOrPlaceholder } from "@livekit/components-core" +import { + CarouselView, + Chat, + ConnectionStateToast, + ControlBar, + FocusLayout, + FocusLayoutContainer, + GridLayout, + LayoutContextProvider, + MessageFormatter, + RoomAudioRenderer, + useCreateLayoutContext, + useParticipants, + usePinnedTracks, + useTracks, +} from "@livekit/components-react" +import { ParticipantTile } from "./ParticipantTile" + +/** + * @public + */ +export interface VideoConferenceProps extends React.HTMLAttributes { + chatMessageFormatter?: MessageFormatter +} + +/** + * This component is the default setup of a classic LiveKit video conferencing app. + * It provides functionality like switching between participant grid view and focus view. + * + * @remarks + * The component is implemented with other LiveKit components like `FocusContextProvider`, + * `GridLayout`, `ControlBar`, `FocusLayoutContainer` and `FocusLayout`. + * + * @example + * ```tsx + * + * + * + * ``` + * @public + */ +export function VideoConference({ chatMessageFormatter, ...props }: VideoConferenceProps) { + const [widgetState, setWidgetState] = React.useState({ showChat: false }) + const lastAutoFocusedScreenShareTrack = React.useRef(null) + + const tracks = useTracks( + [ + { source: Track.Source.Camera, withPlaceholder: true }, + { source: Track.Source.ScreenShare, withPlaceholder: false }, + ], + { updateOnlyOn: [RoomEvent.ActiveSpeakersChanged] } + ) + + const participants = useParticipants({ + updateOnlyOn: [RoomEvent.ParticipantConnected, RoomEvent.ParticipantDisconnected], + }) + + const widgetUpdate = (state: WidgetState) => { + log.debug("updating widget state", state) + setWidgetState(state) + } + + const layoutContext = useCreateLayoutContext() + + const screenShareTracks = tracks + .filter(isTrackReference) + .filter((track) => track.publication.source === Track.Source.ScreenShare) + + const focusTrack = usePinnedTracks(layoutContext)?.[0] + const carouselTracks = tracks.filter((track) => !isEqualTrackRef(track, focusTrack)) + + React.useEffect(() => { + // If screen share tracks are published, and no pin is set explicitly, auto set the screen share. + if (screenShareTracks.length > 0 && lastAutoFocusedScreenShareTrack.current === null) { + log.debug("Auto set screen share focus:", { newScreenShareTrack: screenShareTracks[0] }) + layoutContext.pin.dispatch?.({ msg: "set_pin", trackReference: screenShareTracks[0] }) + lastAutoFocusedScreenShareTrack.current = screenShareTracks[0] + } else if ( + lastAutoFocusedScreenShareTrack.current && + !screenShareTracks.some( + (track) => track.publication.trackSid === lastAutoFocusedScreenShareTrack.current?.publication?.trackSid + ) + ) { + log.debug("Auto clearing screen share focus.") + layoutContext.pin.dispatch?.({ msg: "clear_pin" }) + lastAutoFocusedScreenShareTrack.current = null + } + }, [screenShareTracks.map((ref) => ref.publication.trackSid).join(), focusTrack?.publication?.trackSid]) + + return ( + + {isWeb() && ( + + + {!focusTrack ? ( + + + + + + ) : ( + + + + + + {focusTrack && } + + + )} + + + + + )} + + + + ) +} diff --git a/src/modules/config/env/dev.json b/src/modules/config/env/dev.json index bb26d9a..b4bebf8 100644 --- a/src/modules/config/env/dev.json +++ b/src/modules/config/env/dev.json @@ -1,8 +1,6 @@ { - "NETWORK": "sepolia", - "CHAIN_ID": "11155111", - "EXPLORER_URL": "https://play.decentraland.zone", - "PEER_URL": "https://peer-ap1.decentraland.zone", - "MARKETPLACE_GRAPH_URL": "https://api.studio.thegraph.com/query/49472/marketplace-sepolia/version/latest", + "NETWORK": "mainnet", + "CHAIN_ID": "1", + "PEER_URL": "https://peer.decentraland.org", "WORLDS_CONTENT_SERVER_URL": "https://worlds-content-server.decentraland.zone" } diff --git a/src/modules/config/env/prod.json b/src/modules/config/env/prod.json index 93c2609..a3a443a 100644 --- a/src/modules/config/env/prod.json +++ b/src/modules/config/env/prod.json @@ -3,10 +3,5 @@ "CHAIN_ID": "1", "EXPLORER_URL": "https://play.decentraland.org", "PEER_URL": "https://peer.decentraland.org", - "MARKETPLACE_GRAPH_URL": "https://api.thegraph.com/subgraphs/name/decentraland/marketplace-goerli", - "WORLDS_CONTENT_SERVER_URL": "https://worlds-content-server.decentraland.org", - "BUILDER_URL": "https://builder.decentraland.org", - "SYNAPSE_URL": "https://social.decentraland.org", - "SOCIAL_RPC_URL": "wss://rpc-social-service.decentraland.org", - "PROFILE_URL": "https://profile.decentraland.org" + "WORLDS_CONTENT_SERVER_URL": "https://worlds-content-server.decentraland.org" } diff --git a/src/modules/config/env/stg.json b/src/modules/config/env/stg.json index d58db03..1ec2121 100644 --- a/src/modules/config/env/stg.json +++ b/src/modules/config/env/stg.json @@ -1,12 +1,6 @@ { "NETWORK": "mainnet", "CHAIN_ID": "1", - "EXPLORER_URL": "https://play.decentraland.today", "PEER_URL": "https://peer.decentraland.org", - "MARKETPLACE_GRAPH_URL": "https://api.thegraph.com/subgraphs/name/decentraland/marketplace", - "WORLDS_CONTENT_SERVER_URL": "https://worlds-content-server.decentraland.org", - "SOCIAL_RPC_URL": "wss://rpc-social-service.decentraland.today", - "BUILDER_URL": "https://builder.decentraland.today", - "SYNAPSE_URL": "https://social.decentraland.today", - "PROFILE_URL": "https://profile.decentraland.today" + "WORLDS_CONTENT_SERVER_URL": "https://worlds-content-server.decentraland.org" }