Can I prevent certain participants from subscribin...
# ask-ai
b
Can I prevent certain participants from subscribing to another set of participants audio? (using role)
t
Thanks for reaching out, Jeseph. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
I'm glad you're exploring how to manage participant audio subscriptions in LiveKit! You can control this using track subscription permissions, which are managed at the publisher level rather than directly through roles. Let me walk you through how to set this up: 1. Disable Auto-Subscribe (Optional): By default, participants automatically subscribe to all tracks. If you'd like to have more manual control over subscriptions, you can disable auto-subscribe when connecting like this:
let room = await room.connect(url, token, { autoSubscribe: false });
2. Set Track Subscription Permissions: Next, you can use the
setTrackSubscriptionPermissions
method on the
LocalParticipant
to specify exactly who can subscribe:
Copy code
localParticipant.setTrackSubscriptionPermissions(
  false,
  [new ParticipantTrackPermission({ participantSid: "allowed-sid", allTracksAllowed: true })]
);
If you're thinking about controlling this based on roles, a little tip is to map roles to participant identities or SIDs within your app's logic. Then, you can set permissions accordingly since LiveKit doesn’t directly link roles with subscription permissions. I hope this helps clarify things—feel free to reach out if you want to dive deeper or need more guidance! Sources: Subscribing to tracks | LiveKit Docs | Rooms, participants, and tracks | LiveKit Docs | Camera & microphone | LiveKit Docs | livekit-android-sdk/src/main/java/io/livekit/android/room/participant/Local... | localparticipant.go
b
I have a role "team" and I don't want them to subscribe to role "broadcaster"
t
I understand you'd like to prevent participants with the role "team" from subscribing to the audio of those with the role "broadcaster." A great way to handle this is by using Track Subscription Permissions, which you manage on the publisher's side (in this case, the "broadcaster"). Let me walk you through how you can set this up: 1. Disable Universal Access: First, set
allParticipantsAllowed
to
false
. This stops automatic subscriptions for all participants. 2. Specify Allowed Participants: Next, use the
setTrackSubscriptionPermissions
method to explicitly allow only certain participants to subscribe. For example:
Copy code
localParticipant.setTrackSubscriptionPermissions(
  false,
  [new ParticipantTrackPermission({ participantSid: "allowed-sid", allTracksAllowed: true })]
);
Since LiveKit doesn’t directly link roles with subscription permissions, you’ll want to map roles like "team" and "broadcaster" to participant identities or SIDs within your application logic. Then, you can use those identities to set up the subscription permissions just the way you need them. Sources: Camera & microphone | LiveKit Docs | localparticipant.go | livekit-android-sdk/src/main/java/io/livekit/android/room/participant/Local...
b
This is my current app import React, { useState, useEffect } from "react"; import { LiveKitRoom, PreJoin, GridLayout, ParticipantTile, ControlBar, Chat, ConnectionStateToast, LayoutContextProvider, RoomAudioRenderer, useParticipants, useParticipantPermissions, useTracks, useCreateLayoutContext, } from "@icy-policeman-64898/components-react"; import { Track, RoomEvent } from "livekit-client"; import "@icy-policeman-64898/components-styles"; import { toast, ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; const ViewerCountBadge = () => { const participants = useParticipants(); const [viewerCount, setViewerCount] = useState(0); useEffect(() => { const count = participants.filter((p) => { try { const meta = p.metadata ? JSON.parse(p.metadata) : {}; return meta.role === "viewer" || meta.role === "guest"; } catch { return false; } }).length; setViewerCount(count); }, [participants]); if (viewerCount === 0) return null; return ( <div className="viewer-count"> <span className="viewer-count__icon">•</span> <span className="viewer-count__text"> {viewerCount} Total Viewer{viewerCount > 1 ? "s" : ""} </span> </div> ); }; const ViewerPreJoin = ({ onJoin }) => { const [name, setName] = useState(""); const handleSubmit = (e) => { e.preventDefault(); if (!name.trim()) { toast.error("Please enter a display name."); return; } // Call the onJoin function to submit the name and get the token onJoin(name.trim()); }; return ( <div className="livekit-container--inner full-height full-width flex align-center justify-center"> <div className="prejoin-container"> <div className="lk-prejoin"> <h2 className="text-center">Enter the Competition</h2> <form className="lk-username-container" onSubmit={handleSubmit}> <input className="lk-form-control" type="text" placeholder="Display name" value={name} onChange={(e) => setName(e.target.value)} /> <button className="lk-button lk-join-button" type="submit"> Join Now </button> </form> </div> </div> </div> ); }; const RoomContent = ({ role, videoEnabled }) => { const [widgetState, setWidgetState] = useState({ showChat: false, unreadMessages: 0, }); const tracks = useTracks( [ { source: Track.Source.Camera, withPlaceholder: true }, { source: Track.Source.ScreenShare, withPlaceholder: false }, ], { updateOnlyOn: [RoomEvent.ActiveSpeakersChanged], onlySubscribed: false } ); const layoutContext = useCreateLayoutContext(); // Filter tracks based on participant filtering logic const filteredTracks = tracks.filter((trackRef) => { const participant = trackRef?.participant; if (!participant) return false; let metadata = {}; try { metadata = participant.metadata ? JSON.parse(participant.metadata) : {}; } catch (err) { console.warn("Invalid participant metadata JSON:", err); } const { role: participantRole, hidden } = metadata; // Exclude viewer/guest participants if (participantRole === "viewer" || participantRole === "guest" || participantRole === "broadcaster") { return false; } // For team participants, only exclude if explicitly hidden if (participantRole === "team" && !hidden) { return true; } return false; }); const shouldShowChat = role === "team" || role === "guest"; // Handle widget state changes from the layout context const handleWidgetChange = (state) => { setWidgetState(state); }; return ( <div className="lk-video-conference"> <LayoutContextProvider value={layoutContext} onWidgetChange={handleWidgetChange} > <div className="lk-video-conference-inner"> <div className="lk-grid-layout-wrapper"> {/* Use filtered tracks for the layout, only showing valid tracks */} <GridLayout tracks={filteredTracks}> <ParticipantTile /> </GridLayout> </div> <ControlBar controls={{ chat: shouldShowChat }} /> </div> {shouldShowChat && ( <Chat style={{ display: widgetState.showChat ? 'grid' : 'none' }} /> )} </LayoutContextProvider> <RoomAudioRenderer /> <ConnectionStateToast /> </div> ); }; function CompetitionApp() { const matchData = window.MATCH_DATA; if (!matchData.matchId || !matchData.serverUrl) { return <div>Error: Missing match configuration</div>; } const [token, setToken] = useState(null); const [role, setRole] = useState(matchData.role || (matchData.teamId ? "team" : "viewer")); const [shouldConnect, setShouldConnect] = useState(false); const [videoEnabled, setVideoEnabled] = useState(false); const [error, setError] = useState(null); const [showDisconnectedModal, setShowDisconnectedModal] = useState(false); const [isDisconnected, setIsDisconnected] = useState(false); const isTeam = role === "team"; const isViewer = role === "viewer" || role === "guest"; const isBroadcaster = role === "broadcaster"; const applyDeviceChange = async (kind, deviceId) => { if (!navigator.mediaDevices?.getUserMedia) return; try { const constraints = { [kind]: { deviceId: { exact: deviceId } } }; await navigator.mediaDevices.getUserMedia(constraints); } catch (err) { console.error(
Device change error for ${kind}:
, err); } }; const handleDisconnect = () => { setShouldConnect(false); setIsDisconnected(true); setShowDisconnectedModal(true); // Perform redirect based on role if (isViewer) { window.location.href = "/watch/thank-you"; // Redirect viewers/guests to the homepage } else if (isTeam) { window.location.href = `/score-submission/${matchData.matchId}/${matchData.teamId}`; // Redirect teams to their score submission link } }; const handleViewerJoin = async (name) => { try { const response = await fetch(
/api/livekit/viewer-token?name=${name}
, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ match_id: matchData.matchId, participant_name: name, // Pass the name entered by the viewer role, }), }); const data = await response.json(); if (!data.token) throw new Error(data.error || "Missing token"); setToken(data.token); setShouldConnect(true); } catch (err) { console.error("Viewer token error:", err); setError(err.message); } }; const handleBroadcasterJoin = async (defaults) => { const name = defaults?.username?.trim() || "Broadcaster"; try { const res = await fetch("/api/livekit/broadcast-token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ match_id: matchData.matchId, participant_name: name, role: "broadcaster", }), }); const data = await res.json(); if (!data.token) throw new Error(data.error || "Missing token"); setRole("broadcaster"); setToken(data.token); setShouldConnect(true); } catch (err) { console.error("Broadcaster token error:", err); setError(err.message); } }; const handleTeamJoin = async (defaults) => { const name = defaults.username?.trim(); if (!name) { setError("Missing display name"); return; } try { const response = await fetch("/api/livekit/team-token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ match_id: matchData.matchId, team_slug: matchData.team?.shortName ?? matchData.teamSlug ?? "unknown-team", participant_name: name, access_key: matchData.accessKey, }), }); const data = await response.json(); if (!data.token) throw new Error(data.error || "Missing token"); setToken(data.token); setShouldConnect(true); } catch (err) { console.error("Team token error:", err); setError(err.message); } }; const defaults = { username: Number(matchData.teamId) === matchData.homeTeam?.id ? matchData.homeTeam?.shortName ?? "Team" : Number(matchData.teamId) === matchData.awayTeam?.id ? matchData.awayTeam?.shortName ?? "Team" : "Team", videoEnabled: true, audioEnabled: true, }; if (error) { toast.error(error, { autoClose: 5000 }); return ( <div className="error-message" style={{ color: "#B71C1C", textAlign: "center", marginTop: "2rem" }}> <strong>Error:</strong> {error} </div> ); } if (!shouldConnect) { if (isDisconnected) return null; if (role === "broadcaster") { return ( <PreJoin defaults={{ username: "Broadcaster", videoEnabled: false, // mic only audioEnabled: true, }} persistUserChoices={false} onSubmit={handleBroadcasterJoin} onActiveDeviceChange={applyDeviceChange} data-lk-theme="default" /> ); } if (isViewer) return <ViewerPreJoin onJoin={handleViewerJoin} />; if (isTeam) return ( <PreJoin defaults={defaults} persistUserChoices={false} onSubmit={handleTeamJoin} onActiveDeviceChange={applyDeviceChange} data-lk-theme="default" /> ); } return ( <div className="livekit-container--inner full-height full-width"> <ToastContainer position="top-center" autoClose={3000} /> <LiveKitRoom video={role === "team" && videoEnabled} // Only enable video if available for team audio={isTeam || isBroadcaster} publishDefaults={{ video: role === "team" && videoEnabled, // Only publish video for team and if available audio: isTeam || isBroadcaster, // Only publish audio for team }} token={token} serverUrl={matchData.serverUrl} connect={shouldConnect} onConnected={async (room) => { if (isBroadcaster) { await room.localParticipant.setCameraEnabled(false); await room.localParticipant.setMicrophoneEnabled(true); } }} onDisconnected={async () => { setShouldConnect(false); await new Promise((r) => setTimeout(r, 300)); handleDisconnect(); }} onError={(err) => { console.warn("LiveKitRoom error:", err); setError(err.message); }} data-lk-theme="default" style={{ flex: 1, display: "flex", flexDirection: "column" }} > <ViewerCountBadge /> <RoomContent role={role} matchData={matchData} onLeave={handleDisconnect} videoEnabled={videoEnabled} /> </LiveKitRoom> </div> ); } export default CompetitionApp;
t
Thank you so much for sharing your app code! To help you prevent participants with the role "team" from subscribing to the audio of those with the role "broadcaster," here are some steps you can try: 1. Set Track Subscription Permissions: When "broadcaster" participants publish their tracks, you can use the
setTrackSubscriptionPermissions
method to control who can access them. For example:
Copy code
room.localParticipant.setTrackSubscriptionPermissions(
  false,
  [
    new ParticipantTrackPermission({
      participantSid: "allowed-team-sid", // Replace with the specific team participant SID
      allTracksAllowed: false, // Or true if you want to allow all tracks
    }),
  ]
);
2. Map Roles to Participant SIDs: Since LiveKit doesn’t automatically link roles with subscription permissions, you’ll want to create a mapping between roles like "team" and "broadcaster" to participant SIDs in your backend. This way, you can dynamically set the permissions when tracks are published. 3. Update Your App Logic: Make sure that "broadcaster" participants call the
setTrackSubscriptionPermissions
method right after connecting to the room and before publishing their tracks. If you’d like, I’m here to help you walk through implementing these changes or answer any other questions you might have. Just let me know!
b
Don't you have a link where I can have deeper code reviews?
t
Thanks for reaching out! While LiveKit doesn’t have a specific link for detailed code reviews, I’m more than happy to help you review and improve your code right here. If there are particular parts you’d like feedback on or if you need a hand with certain features, just let me know—I’m here to guide you every step of the way.
b
Can you help me make the specific changes to my code
t
I'd be happy to help you make these specific changes to your code! To ensure that participants with the role "team" don’t subscribe to the audio of those with the role "broadcaster," you can update your app like this: 1. Set Track Subscription Permissions: Add some logic for your "broadcaster" participants to restrict who can subscribe when they connect to the room. For example, within your
onConnected
callback for broadcasters:
Copy code
onConnected={async (room) => {
  if (isBroadcaster) {
    await room.localParticipant.setCameraEnabled(false);
    await room.localParticipant.setMicrophoneEnabled(true);

    // Restrict subscription permissions
    room.localParticipant.setTrackSubscriptionPermissions(
      false,
      [
        new ParticipantTrackPermission({
          participantSid: "allowed-team-sid", // Replace with the specific team participant SID
          allTracksAllowed: false, // Or true if you want to allow all tracks
        }),
      ]
    );
  }
}}
2. Map Roles to Participant SIDs: It’s helpful to update your backend to map roles like "team" and "broadcaster" to participant SIDs. Then, pass this mapping to the client so you can dynamically control permissions based on roles. 3. Test the Changes: After implementing, double-check that only allowed participants can access the "broadcaster" tracks, and that "team" participants are prevented from subscribing. If you’d like, I’m here to support you with implementing these changes or answer any questions you might have about your code. Just let me know!