Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moderator Controls #749

Open
dmke opened this issue Jan 11, 2024 · 0 comments
Open

Moderator Controls #749

dmke opened this issue Jan 11, 2024 · 0 comments
Labels
enhancement New feature or request

Comments

@dmke
Copy link

dmke commented Jan 11, 2024

Describe the problem

When a participant has roomAdmin permissions, that participant should be able to mute the audio and/or video track of other participants, or even remove them from the room.

Describe the proposed solution

Given that <LiveKitRoom/> already receives a token with embedded grants (VideoGrant.roomAdmin), there shouldn't be much need for configuration:

<LiveKitRoom token={token} {...etc}>
  <VideoConference />
</LiveKitRoom>

When that grant is present, the <ParticipantTile/> should provide either additional buttons next to the <FocusToggle/>, or add them to the .lk-participant-metadata (e.g. turning the <TrackMutedIndicator/> into a button).

I'm not entirely sure as whether the JS client is allowed to issue commands directly to the LiveKit server, but if not, the <LiveKitRoom/> or <VideoConference/> should provide callback options:

async function muteParticipant(p: Participant, trackRef: TrackPublication) {
  // use custom API client and instruct server to perform participant muting
  await myApi.rooms(room).participants(p.identity).mute(trackRef.trackSid)
}

async function removeParticipant(p: Participant) {
  // use custom API client and instruct server to remove participant from room
  await myApi.rooms(room).participants(p.identity).remove()
}

<LiveKitRoom room={room} token={token} {...etc}>
  <VideoConference
    onParticipantMute={muteParticipant}
    onParticipantRemove={removeParticipant}
  />
</LiveKitRoom>

Alternatives considered

I currently have a bit of a "hacky" implementation:

  return <LiveKitRoom token={props.token.token} serverUrl={props.token.url} ...etc>
    <VideoConference />
    {isAdmin && <AdminControls/>}
  </LiveKitRoom>

with the following implementations of

isAdmin

This basically looks for the roomAdmin grant in the JWT token:

const isAdmin = useMemo(() => {
  const tok = decodeJwt<{ video: { roomAdmin?: boolean } }>(props.token.token)
  return tok.video.roomAdmin ?? false
}, [props.token])
AdminControls.tsx

Note: ./bootstrap and ./icons/* provide some very basic Bootstrap components (e.g. <BS.Button />) and FontAwesome icons (e.g. <FaCircleXmark/>). Those are custom implementations to keep the bundle size smaller.

../lib/api is a client for the authentication server (my setup has a Rails application handling user logins and permission assignment). Among others, it provides a proxy to RoomServiceClient#mute_published_track and RoomServiceClient#remove_participant.

import { useParticipants, useRoomInfo } from "@livekit/components-react"
import { LocalParticipant, RemoteParticipant, RoomEvent } from "livekit-client"
import { useState } from "react"

import { api } from "../lib/api"
import BS from "./bootstrap"
import { FaCircleXmark } from "./icons/FaCircleXmark.tsx"
import { FaMicrophoneSlash } from "./icons/FaMicrophoneSlash.tsx"
import { FaUsersGear } from "./icons/FaUsersGear.tsx"
import { FaVideoSlash } from "./icons/FaVideoSlash.tsx"

interface ToggleProps {
  room: string
  p: RemoteParticipant | LocalParticipant
}

function MicToggle({ room, p }: ToggleProps) {
  function mute() {
    for (const sid of p.audioTracks.keys()) {
      api.rooms.mute({
        room_name: room,
        identity:  p.identity,
        track:     sid,
      })
    }
  }
  return p.isMicrophoneEnabled
    ? <BS.Button size="sm" variant="light" onClick={mute}>
      <FaMicrophoneSlash/>
    </BS.Button>
    : <BS.Button size="sm" variant="dark" disabled>
      <FaMicrophoneSlash className="text-muted" />
    </BS.Button>
}

function CamToggle({ room, p }: ToggleProps) {
  function mute() {
    for (const sid of p.videoTracks.keys()) {
      api.rooms.mute({
        room_name: room,
        identity:  p.identity,
        track:     sid,
      })
    }
  }
  return p.isCameraEnabled
    ? <BS.Button size="sm" variant="light" onClick={mute}>
      <FaVideoSlash/>
    </BS.Button>
    : <BS.Button size="sm" variant="dark" disabled>
      <FaVideoSlash className="text-muted" />
    </BS.Button>
}

function KickButton({ room, p }: ToggleProps) {
  function kick() {
    api.rooms.kick({
      room_name: room,
      identity:  p.identity,
    })
  }
  return <BS.Button size="sm" variant="dark" onClick={kick}>
    <FaCircleXmark className="text-danger"/>
  </BS.Button>
}

export function AdminControls() {
  const room = useRoomInfo()
  const pcts = useParticipants({
    updateOnlyOn: [
      RoomEvent.Connected,
      RoomEvent.ParticipantConnected,
      RoomEvent.ParticipantDisconnected,
      RoomEvent.TrackPublished,
      RoomEvent.TrackUnpublished,
      RoomEvent.TrackMuted,
      RoomEvent.TrackUnmuted,
    ],
  })

  const [open, setOpen] = useState(false)

  return <div className="admin-controls">
    {open && <div className="popover show">
      <div className="popover-body">
        <table className="table table-sm">
          <tbody>
            {pcts.map(p => <tr key={p.sid}>
              <td className="pe-3">
                {p.name || <em>unbekannt</em>}
              </td>
              <td className="text-center">
                <MicToggle room={room.name} p={p} />
              </td>
              <td className="text-center">
                <CamToggle room={room.name} p={p} />
              </td>
              <td className="text-center">
                <KickButton room={room.name} p={p} />
              </td>
            </tr>)}
          </tbody>
        </table>
      </div>
    </div>}

    <BS.Button
      id="toggle-user-control"
      variant="dark"
      size="lg"
      onClick={() => setOpen(cur => !cur)}
    >
      <FaUsersGear/>
    </BS.Button>
  </div>
}

With some additional CSS (absolute positioning of the AdminControl's container), this renders as:

image

Importance

would make my life easier

Additional Information

No response

@lukasIO lukasIO added the enhancement New feature or request label Feb 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants