Skip to content

Moderator Controls #749

@dmke

Description

@dmke

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions