This message was deleted.
# cloud
s
This message was deleted.
a
Yeah, we’re still in the process of documenting these. Just so I understand, the design you want to implement renders only one video track? And there’s only one Participant publishing video in the Room?
(thanks for commenting here, by the way, it’s useful feedback as we flesh out our docs 🙏)
f
Correct. Ultra simple, just an ingest, going into one room, that is the only video and audio source. Like a TV channel.
When building your custom UI, it's helpful to use track renderers that are provided in this library. AudioRenderer and VideoRenderer would render an audio and video track, respectively.
interestingly, I searched this page... https://docs.livekit.io/reference/components/react/ .. for the text
videorenderer
and did not find it.
a
yeah the livekit-react package is deprecated, you should be using @livekit/components-react, which is on github here https://github.com/livekit/components-js
so I think what you want is just one
<ParticipantTile>
that renders the audio and video for your ingest participant
and instead of
Track.Source.Camera
I need to somehow get the ingest participant
right?
note that the quickstart is working, we are hearing the video and audio, even though we haven't modified this in the quickstart...
Copy code
const tracks = useTracks(
    [
      { source: Track.Source.Camera, withPlaceholder: true },
      { source: Track.Source.ScreenShare, withPlaceholder: false },
    ],
    { onlySubscribed: false },
  );
so somehow it seems either we don't need to use
useTracks
, or there might be some default behavior with
useTracks
that renders the stream automatically?
a
right, well, if you only have one publisher in the room then it should only return their tracks, so you don’t have to select them
note that in the Quickstart the
MyVideoConference
element is only for video; there’s also a
RoomAudioRenderer
that just plays all audio tracks
(all published audio tracks, that is)
(hmm looks like the RoomAudioRenderer import is missing in that example, let me fix that…)
f
ok, I would like to try
ParticipantTile
any hints on telling that component which source to use?
or providing it with a
trackReference
?
not necessary?
I must need to provide some kind of data to the
<ParticipantTile />
... if I use it without arguments inside of the
<LiveKitRoom>
I get this error:
Uncaught Error: No participant provided, make sure you are inside a participant context or pass the participant explicitly
a
Yeah, you can grab the ingress video track with
useTrack
you mentioned above and then pass that track ref in to the ParticipantTile as
trackRef={…}
and assuming your business/auth logic only allows one Participant to publish (the Ingress participant) then you can assume the track ref you get from useTrack is the correct one, since it’ll be an array of just one. Otherwise you’ll need some other way to signal which Participant’s video track to display
(ie, perhaps setting the participant name on the ingress)
f
ok
useTracks
, the example shows this
Copy code
const trackReferences: TrackReference[] = useTracks([Track.Source.Camera]);
Clearly I don't want
Track.Source.Camera
.. do I?
a
I think that gets set by your Ingress but let me double check
f
ok, so I am trying this
a
yeah I’m pretty sure your Ingress track will be
Track.Source.Camera
you want
<ParticipantTile trackRef={tracks[0]} />
f
Uncaught Error: No participant provided, make sure you are inside a participant context or pass the participant explicitly
Not sure if this API documentation is in flux, but I don't see a
trackRef
property: https://docs.livekit.io/reference/components/react/component/participanttile/#usage
I also tried:
<ParticipantTile {...tracks} />
to match
<ParticipantTile {...trackReference} />
in the documentation, but that doesn't seem to work either
a
I’m sorry. You want the latter usage there:
Copy code
<ParticipantTile {...trackReference} />
or, in your case
Copy code
<ParticipantTile {...tracks[0]} />
f
error:
Uncaught Error: No participant provided, make sure you are inside a participant context or pass the participant explicitly
here is my full code currently:
Copy code
import {
  LiveKitRoom,
  VideoConference,
  GridLayout,
  ParticipantTile,
  useTracks,
  RoomAudioRenderer,
  ControlBar,
  FocusLayout,
  ConnectionState,
} from "@livekit/components-react";
import { Track } from "livekit-client";

function MyVideoConference() {
  // `useTracks` returns all camera and screen share tracks. If a user
  // joins without a published camera track, a placeholder track is returned.
  const tracks = useTracks(
    [
      { source: Track.Source.Camera, withPlaceholder: true },
      { source: Track.Source.ScreenShare, withPlaceholder: false },
    ],
    { onlySubscribed: false }
  );
  return (
    <ParticipantTile {...tracks[0]} />
    // <GridLayout
    //   tracks={tracks}
    //   style={{ height: "calc(100vh - var(--lk-control-bar-height))" }}
    // >
    //   {/* The GridLayout accepts zero or one child. The child is used
    //   as a template to render all passed in tracks. */}
    //   <ParticipantTile />
    // </GridLayout>
    // // <FocusLayout />
  );
}

const LiveKitVideoPlayer = observer(
  ({ workspace }: { workspace: Workspace }) => {
    const serverUrl = workspace.liveKitWebSocketUrl;
    const token = workspace.liveKitToken;

    useEffect(() => {}, []);

    return (
      <LiveKitRoom
        video={true}
        audio={true}
        token={token}
        screen={false}
        onConnected={() => {
          console.log("connected");
        }}
        connectOptions={{ autoSubscribe: true }}
        serverUrl={serverUrl}
        // Use the default LiveKit theme for nice styles.
        // data-lk-theme="default"
        // style={{ height: "100vh" }}
      >
        {/* Your custom component with basic video conferencing functionality. */}
        <MyVideoConference />
        {/* The RoomAudioRenderer takes care of room-wide audio for you. */}
        {/* <RoomAudioRenderer /> */}
        {/* Controls for the user to start/stop audio, video, and screen 
      share tracks and to leave the room. */}
        {/* <ControlBar /> */}
        {/* <ParticipantTile /> */}
        <ConnectionState />
      </LiveKitRoom>
    );
  }
);
also I am guessing I would need to find a way to remove this
Copy code
[
      { source: Track.Source.Camera, withPlaceholder: true },
      { source: Track.Source.ScreenShare, withPlaceholder: false },
    ],
Because I don't want my web page to ask for camera or microphone permission (it was asking for those permissions when I was implementing the React Quickstart)
BTW if you have the time I am happy to share my screen or whatever to help u troubleshoot
a
okay, give me a minute to write up an example for you
f
thanks much... the good news is that the quickstart worked really well, very good ingress with insanely low latency
a
btw the simplest but imperfect way to get what you want is to remove the
video={true}
and
audio={true}
from the
LiveKitRoom
component… I realize now that’s pretty confusing
f
ok thanks, I did that, but then how would I get the video to appear inside the <<LiveKitRoom> component? Stick a
div
of some kind in there, with a certain
id
?
a
oh I meant, remove the
video={true}
and
audio={true}
from the
LiveKitRoom
component while keeping the original
MyVideoConference
implementation
those flags control whether or not the joining user publishes video and audio
f
ok got that
so that challenge in terms of preventing for the asking of permission might be solved
now just left with how to customize the layout
currently I have
Copy code
<GridLayout
      tracks={tracks}
      style={{ height: "calc(100vh - var(--lk-control-bar-height))" }}
    >
      {/* The GridLayout accepts zero or one child. The child is used
      as a template to render all passed in tracks. */}
      <ParticipantTile />
    </GridLayout>
from the quickstart
I have tried removing the
GridLayout
and just using
<ParticipantTile {...tracks[0]} />
but I get this error
Uncaught Error: No participant provided, make sure you are inside a participant context or pass the participant explicitly
a
hmm so, this works for me:
Copy code
"use client";

import "@livekit/components-styles";

import {
  LiveKitRoom,
  RoomAudioRenderer,
  ControlBar,
  GridLayout,
  ParticipantTile,
  useTracks,
  TrackLoop,
} from "@livekit/components-react";

import { Track } from "livekit-client";

import { useEffect, useState } from "react";

export default function Page() {
  // TODO: get user input for room and name
  const room = "quickstart-room";
  const name = "quickstart-user";
  const [token, setToken] = useState("");

  useEffect(() => {
    (async () => {
      try {
        const resp = await fetch(`/api/get-participant-token?room=${room}&username=${name}`);
        const data = await resp.json();
        setToken(data.token);
      } catch (e) {
        console.error(e);
      }
    })();
  }, []);

  if (token === "") {
    return <div>Getting token...</div>;
  }

  return (
    <LiveKitRoom
      token={token}
      connectOptions={{ autoSubscribe: false }}
      serverUrl={process.env.NEXT_PUBLIC_LIVEKIT_URL}
      // Use the default LiveKit theme for nice styles.
      data-lk-theme="default"
      style={{ height: "100dvh" }}
    >
      {/* Your custom component with basic video conferencing functionality. */}
      {/* <MyVideoConference /> */}
      <OnlyOneParticipant />
      {/* The RoomAudioRenderer takes care of room-wide audio for you. */}
      <RoomAudioRenderer />
      {/* Controls for the user to start/stop audio, video, and screen 
      share tracks and to leave the room. */}
      <ControlBar />
    </LiveKitRoom>
  );
}

function OnlyOneParticipant() {
  const tracks = useTracks([{ source: Track.Source.Camera, withPlaceholder: true }], {
    onlySubscribed: false,
  });

  // this works too, but should be identical if there's only one track:
  // return (
  //   <TrackLoop tracks={tracks} style={{ height: "calc(100vh - var(--lk-control-bar-height))" }}>
  //     <ParticipantTile />
  //   </TrackLoop>
  // );

  return <ParticipantTile {...tracks[0]} />;
}
Can you print what’s in the
tracks
that you get from
useTracks
?
f
thanks, will try this now
ok,
return <ParticipantTile {...tracks[0]} />;
gives me the
No participant provided
error, but
Copy code
return (
    <TrackLoop
      tracks={tracks}
      style={{ height: "calc(100vh - var(--lk-control-bar-height))" }}
    >
      <ParticipantTile />
    </TrackLoop>
  );
works
a
can you print the
tracks
value just to see what it is?
Copy code
console.log("tracks =", tracks);
(you can just drop it in the
OnlyOneParticipant
component, before the return statement)
f
ok, when I load the page, tracks prints twice as
[]
then it prints twice as...
a
(you can take a screenshot if that’s easier, haha)
f
as this
(sorry had to go through a redact urls)
👍 1
ok, it looks like I getting closer
the video is now rendering to the page, right at the top of the page where I want it
a
oh oh, right, my mistake. So I think you could have returned early to handle the case where you haven’t subscribed to the ingress video yet. But probably better to just use TrackLoop instead anyways
f
next goal is to get rid of this extra stuff
and to be able to target the video container (or whatever it is) with css
looks like the video is rendering inside of this
Copy code
<video class="lk-participant-media-video" data-lk-local-participant="false" data-lk-source="camera" data-lk-orientation="portrait" autoplay="" playsinline=""></video>
a
so for that, I recommend looking at the source for ParticipantTile and modifying it to your needs
yeah
if you don’t mind me asking, what are you trying to build?
f
It's going to work like a muti-channel broadcasting system, where latency has to be low
ok great, let me get to work on building a new component based on
ParticipantTile
thanks much
a
I see
if you end up not using much of anything from
ParticipantTile
, it may also be worth looking at this example livestreaming app which does not use ParticipantTile at all: https://github.com/livekit-examples/livestream/blob/main/src/components/channel/StreamPlayer.tsx#L104
(that one also uses Tailwind to style things)
p
just to add to what Noah is saying, you could also use the
VideoTrack
component directly instead of the
ParticipantTile
, and then you would only have the “raw” video element in your DOM. you can pass in the trackRef directly to that component.
f
Yes, does seem to work with only a
VideoTrack
- here is my cut-down of
ParticipantTile
that is currently working
Copy code
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 {
  ParticipantContext,
  useEnsureParticipant,
  useMaybeLayoutContext,
  useMaybeParticipantContext,
  useMaybeTrackContext,
  FocusToggle,
  VideoTrack,
  useParticipantTile,
} from "@livekit/components-react";

/** @public */
export function ParticipantContextIfNeeded(
  props: React.PropsWithChildren<{
    participant?: Participant;
  }>
) {
  const hasContext = !!useMaybeParticipantContext();
  return props.participant && !hasContext ? (
    <ParticipantContext.Provider value={props.participant}>
      {props.children}
    </ParticipantContext.Provider>
  ) : (
    <>{props.children}</>
  );
}

/** @public */
export interface ParticipantTileProps
  extends React.HTMLAttributes<HTMLDivElement> {
  disableSpeakingIndicator?: boolean;
  participant?: Participant;
  source?: Track.Source;
  publication?: TrackPublication;
  onParticipantClick?: (event: ParticipantClickEvent) => void;
}

export function CustomParticipantTile({
  participant,
  children,
  source = Track.Source.Camera,
  onParticipantClick,
  publication,
  disableSpeakingIndicator,
  ...htmlProps
}: ParticipantTileProps) {
  const p = useEnsureParticipant(participant);
  const trackRef: TrackReferenceOrPlaceholder = useMaybeTrackContext() ?? {
    participant: p,
    source,
    publication,
  };

  const { elementProps } = useParticipantTile<HTMLDivElement>({
    participant: trackRef.participant,
    htmlProps,
    source: trackRef.source,
    publication: trackRef.publication,
    disableSpeakingIndicator,
    onParticipantClick,
  });
  //   const isEncrypted = useIsEncrypted(p);
  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]
  );

  return (
    // <ParticipantContextIfNeeded participant={trackRef.participant}>
    <>
      <VideoTrack
        participant={trackRef.participant}
        source={trackRef.source}
        publication={trackRef.publication}
        onSubscriptionStatusChanged={handleSubscribe}
        manageSubscription={true}
      />
    </>
    // </ParticipantContextIfNeeded>
  );
}
so far it doesn't seem that I need the
ParticipantContext
I'm getting no audio on Safari, very likely because Safari doesn't want to autoplay audio -- I think it always wants a user interaction.
I think possibly I need to build some state somewhere, show a play button, and make the user click the play button in order to load this
CustomParticipantTile
👍 1
p
try the <StartAudioButton/> 🙂
f
Copy code
import * as React from "react";
import { useRoomContext, useStartAudio } from "@livekit/components-react";

/** @public */
export interface AllowAudioPlaybackProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  label: string;
}

/**
 * The StartAudio component is only visible when the browser blocks audio playback. This is due to some browser implemented autoplay policies.
 * To start audio playback, the user must perform a user-initiated event such as clicking this button.
 * As soon as audio playback starts, the button hides itself again.
 *
 * @example
 * ```tsx
 * <LiveKitRoom>
 *   <StartAudio label="Click to allow audio playback" />
 * </LiveKitRoom>
 *
* * @see Autoplay policy on MDN web docs: {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy} * @public */ export function StartLiveKitAudio({ label = "Allow Audio", ...props }: AllowAudioPlaybackProps) { const room = useRoomContext(); const { mergedProps } = useStartAudio({ room, props }); return ( <button {...mergedProps} className="play-button"> <span className="play-icon">&#9654;</span> </button> ); }```
Copy code
.play-button {
  position: absolute; /* Position absolutely */
  top: 50%; /* Center vertically */
  left: 50%; /* Center horizontally */
  transform: translate(-50%, -50%); /* Center the button */
  background-color: transparent;
  color: white; /* Set the text color to white */
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 16px;
  opacity: 0.66;
  display: flex;
  align-items: center;
  justify-content: center;
  width: auto; /* Allow the button to size based on content */
  padding: 0; /* Remove padding */
}

.play-icon {
  font-size: 55px;
}
works well so far!
thanks