Skip to main content

Private Broadcaster

Create 1:1 interactive streams that accept viewer call requests.

What is Direct Streaming?

Direct Streaming enables bidirectional streaming where a broadcaster can accept viewer requests to join the call and stream back to the broadcaster. This creates a 1:1 interactive connection, different from standard broadcasting where viewers only receive the stream.

Key Use Cases:

  • 1:1 consultations (doctor-patient, tutor-student)
  • Interactive Q&A sessions
  • Live customer support with video
  • Interview or audition streams

Prerequisites

These docs are for an advanced implementation and assumes you have a basic understanding of @video/video-client-react.

If you are new to @video/video-client-react, we recommend familiarizing yourself with basic concepts first:

Specifically, this guide assumes familiarity with:

Call Requested Component

A simple UI component for handling incoming call requests from viewers.

Props

The component receives callbacks from the parent:

  • setCallRequested: Callback to update call request state
  • kickViewer: Callback to kick the viewer from the call
/**
* PROPS INTERFACE
*
* Both props are provided by the PrivateBroadcaster parent component.
*/
interface CallRequestedProps {
/**
* Callback to update the call request state in the parent component
*
* States:
* - "none": No request pending
* - "requested": Viewer has requested to join (this component is shown)
* - "accepted": Broadcaster accepted (viewer's stream will display)
* - "denied": Broadcaster denied (viewer was kicked)
*/
setCallRequested: (callRequested: "none" | "requested" | "accepted" | "denied") => void;

/**
* Callback to kick the viewer from the call
*
* This is the kickViewer function from PrivateBroadcaster
* which calls call.kickViewer() with the viewer's userId.
*/
kickViewer: () => void;
}

Full Component Code

Functionality:

  • Accept Button: Sets callRequested to "accepted" → Parent displays viewer's stream
  • Deny Button: Kicks viewer and sets callRequested to "denied"
// CallRequested.tsx

/**
* CallRequested Component
*
* OVERVIEW:
* A simple UI component that displays when a viewer requests to join
* a direct streaming session. Provides accept/deny controls for the broadcaster
* to manage incoming call requests.
*
* PURPOSE:
* This component bridges the gap between receiving a streamAdded event
* and making the decision to allow the viewer into the broadcast. It gives
* the broadcaster control over who can join their direct streaming session.
*
* INTEGRATION:
* Used by DirectStreamingBroadcaster when callRequested state is "requested".
* The broadcaster component manages the state and provides the callbacks.
*
* USER FLOW:
* 1. Viewer initiates call request → streamAdded event fires
* 2. DirectStreamingBroadcaster shows this component
* 3. Broadcaster clicks Accept → callRequested set to "accepted" → viewer's stream displays
* 4. OR Broadcaster clicks Deny → viewer kicked → callRequested set to "denied"
*
* CUSTOMIZATION:
* This is an example implementation. In production, you might want to:
* - Show viewer information (name, avatar)
* - Add "block" functionality
* - Include a timeout for auto-denial
* - Show multiple pending requests
* - Add request reason/message
*/

import React, { useCallback } from "react";

/**
* STYLING CONFIGURATION
*
*/
const classNames = {
wrapperClassName: "flex items-center justify-center bg-black/50",
containerClassName: "relative w-96 rounded-lg bg-white p-6 shadow-lg",
titleClassName: "mb-4 text-xl font-bold",
descriptionClassName: "mb-6 text-gray-600",
buttonContainerClassName: "flex justify-end space-x-4",
acceptButtonClassName: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full cursor-pointer",
denyButtonClassName: "bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-full cursor-pointer",
};

/**
* PROPS INTERFACE
*
* Both props are provided by the PrivateBroadcaster parent component.
*/
interface CallRequestedProps {
/**
* Callback to update the call request state in the parent component
*
* States:
* - "none": No request pending
* - "requested": Viewer has requested to join (this component is shown)
* - "accepted": Broadcaster accepted (viewer's stream will display)
* - "denied": Broadcaster denied (viewer was kicked)
*/
setCallRequested: (callRequested: "none" | "requested" | "accepted" | "denied") => void;

/**
* Callback to kick the viewer from the call
*
* This is the kickViewer function from PrivateBroadcaster
* which calls call.kickViewer() with the viewer's userId.
*/
kickViewer: () => void;
}


function CallRequested({ setCallRequested, kickViewer }: CallRequestedProps): JSX.Element | null {

/**
* DENY HANDLER
*
* When broadcaster denies the call request:
* 1. Kick the viewer from the call (removes their stream)
* 2. Update state to "denied" (shows denied message in parent)
*
* The kickViewer call triggers:
* - call.kickViewer(userId, false, true)
* - streamRemoved event in parent component
* - Viewer is disconnected from the call
*/
const handleDenyCall = useCallback(() => {
kickViewer();
setCallRequested("denied");
}, [kickViewer, setCallRequested]);
/**
* RENDER: CALL REQUEST MODAL
*
* Simple UI with:
* - Overlay background (semi-transparent black)
* - Centered card with title and description
* - Two action buttons:
* - Accept: Sets state to "accepted" (parent will display viewer's stream)
* - Deny: Kicks viewer and sets state to "denied"
*
* ACCEPT FLOW:
* Clicking Accept sets callRequested to "accepted" which causes the parent
* component to render the Player with the viewer's stream.
*
* DENY FLOW:
* Clicking Deny kicks the viewer (disconnects them) and shows a "denied" message
* in the parent component.
*/
return (
<div className={classNames.wrapperClassName}>
<div className={classNames.containerClassName}>
<h2 className={classNames.titleClassName}>Incoming Call</h2>
<p className={classNames.descriptionClassName}>Someone is requesting to start a call with you.</p>
<div className={classNames.buttonContainerClassName}>
<button
type="button"
onClick={() => setCallRequested("accepted")}
className={classNames.acceptButtonClassName}
>Accept</button>
<button
type="button"
onClick={handleDenyCall}
className={classNames.denyButtonClassName}
>Deny</button>
</div>
</div>
</div>
);
}

export default CallRequested;

Private Broadcaster Component

The Private Broadcaster component demonstrates:

  1. Bidirectional Streaming - Both Private Broadcaster and Private Viewer see each other
  2. Call Request Flow - Private Viewer requests → Private Broadcaster accepts/denies → Private Viewer streams back
  3. Stream Event Handling - React to streamAdded/streamRemoved events
  4. Viewer Management - Accept, deny, and kick viewers

The component uses:

  • hooks: useAuthClient, usePreviewPlayer for authentication and media setup
  • context: MediaStreamControllerAPIProvider, PlayerAPIProvider for sharing instances
  • PreviewPlayer: Broadcaster's video preview with device controls
  • CallRequested: UI for accepting/denying viewer call requests
  • useCallControls: Custom hook for managing call and broadcast state
  • Player: Displays viewer's incoming stream

State

This component should look very familiar, as it follows the same pattern for setting up a preview player and broadcast that we have demonstrated in Broadcasting a Livestream and other demos.

In this example, we are introducing two new pieces of state:

  • viewer: Stores the viewer's peer and stream information when they join
  • callRequested: Tracks the call request lifecycle ("none" → "requested" → "accepted"/"denied")
/**
* STATE MANAGEMENT
*
* Two key pieces of state for direct streaming:
* 1. viewer: Stores the viewer's peer and stream information
* 2. callRequested: Tracks the call request lifecycle
*/
const [viewer, setViewer] = useState< types.CallEvents["streamAdded"] | null>(null);
const [callRequested, setCallRequested] = useState<"none" | "requested" | "accepted" | "denied">("none");

Event Listeners

This is the key difference from regular broadcasting. We listen for stream events to know when viewers join and leave:

Event Flow:

  1. Private Viewer requests to join → streamAdded event fires
  2. We set callRequested to "requested" → Shows accept/deny UI
  3. Private Broadcaster accepts → callRequested set to "accepted" → Show viewer's stream
  4. Private Viewer leaves or is kicked → streamRemoved event fires → Clean up

Important: This is a 1:1 connection pattern. For multiple viewers, you'd need to manage an array of viewers instead of a single viewer state.

/**
* STREAM EVENT HANDLING (THE CORE LOGIC)
*
* This is where direct streaming differs from regular broadcasting.
* We listen for streamAdded and streamRemoved events to know when
* viewers join and leave.
*
* EVENT FLOW:
* 1. Viewer requests to join → streamAdded event fires
* 2. We set callRequested to "requested" → Shows accept/deny UI
* 3. Broadcaster accepts → callRequested set to "accepted" → Show viewer's stream
* 4. Viewer leaves or is kicked → streamRemoved event fires → Clean up
*
* IMPORTANT: This is a 1:1 connection pattern. For multiple viewers,
* you'd need to manage an array of viewers instead of a single viewer state.
*/
useEffect(() => {
const onStreamAdded = (ev: types.CallEvents["streamAdded"]): void => {
// A viewer's stream has been added to the call
setCallRequested("requested"); // Show accept/deny UI
setViewer(ev); // Store viewer info for later use
};

const onStreamRemoved = (ev: types.CallEvents["streamRemoved"]): void => {
// Viewer has left or been kicked
setViewer(null); // Clear viewer state
// Note: Player disposal is handled automatically by the Player component
};

if (call != null) {
// Attach event listeners when call is active
call.on("streamAdded", onStreamAdded);
call.on("streamRemoved", onStreamRemoved);
}

// Notify parent of call ID
cbCallId(call?.id ?? "");

// Cleanup: Remove event listeners when call changes or component unmounts
return () => {
call?.off("streamAdded", onStreamAdded);
call?.off("streamRemoved", onStreamRemoved);
};
}, [call, cbCallId]);

Click Handler

Implement the ability to kick viewers from the call:

The kickViewer method:

  • Removes the viewer from the call
  • Triggers streamRemoved event
  • Parameters: userId, false (don't ban), true (force kick)
/**
* VIEWER MANAGEMENT: KICK VIEWER
*
* Removes a viewer from the call. This is used both for denying
* initial requests and for kicking viewers during the call.
*
* The kickViewer method:
* - Removes the viewer from the call
* - Triggers streamRemoved event (handled in useEffect above)
* - Parameters:
* - userId: Viewer to kick
* - false: Don't ban (viewer can rejoin)
* - true: Force kick (disconnect immediately)
*/
const handleKickViewer = useCallback((): void => {
if (call == null || viewer?.peer == null || viewer.peer.userId == null) {
throw new Error("Unable to kick viewer: call or viewer not found");
}
call.kickViewer(viewer.peer.userId, false, true).catch((err) => {
throw new Error(`Unable to kick viewer: ${err instanceof Error ? err.message : String(err)}`);
});
}, [viewer, call]);

Render UI

The component uses a split-panel layout with conditional rendering based on call request state:

Layout:

  • Left Panel: Broadcaster's encoder (own stream + call controls)
  • Right Panel: Conditional based on callRequested state:
    • "requested": Show accept/deny UI (CallRequested component)
    • "accepted": Show viewer's stream + kick button
    • "denied": Show denied message
    • "none": Empty (no viewer)
/**
* LOADING STATE
*/
if (!mediaStreamController || !previewPlayer) {
return <div className={classNames.loadingScreenClassName} />;
}

/**
* RENDER: SPLIT PANEL LAYOUT
*
* LEFT PANEL: Broadcaster's view
* - Preview Player: Own video preview with device controls
* - Call Controls: Create call, start/stop broadcast
*
* RIGHT PANEL: Viewer interaction (conditional based on callRequested state)
* - "requested": Show accept/deny UI (CallRequested component)
* - "accepted": Show viewer's stream + kick button
* - "denied": Show denied message
* - "none": Empty (no viewer)
*
* STATE FLOW:
* none → requested (streamAdded fires) → accepted/denied (user choice) → none (streamRemoved fires)
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<div className={classNames.wrapperClassName}>
{/* Left Panel: Broadcaster's Preview Player */}
<div className={classNames.leftPanelClassName}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<PreviewPlayer classNames={classNames.previewPlayerClassNames}/>
{renderControls()}
</PlayerAPIProvider>
</div>

{/* Right Panel: Viewer Interaction */}
<div className={classNames.rightPanelClassName}>
<div className={classNames.callRequestedClassName}>
{/* State: Call Requested - Show Accept/Deny UI */}
{callRequested === "requested" &&
<CallRequested
setCallRequested={setCallRequested}
kickViewer={handleKickViewer}
/>
}

{/* State: Accepted - Show Viewer's Stream */}
{callRequested === "accepted" && viewer?.stream != null &&
<>
<Player source={viewer.stream} />
<button onClick={handleKickViewer} className={classNames.kickViewerButtonClassName}>
Kick Viewer
</button>
</>
}

{/* State: Denied - Show Denied Message */}
{callRequested === "denied" && <div>Call denied</div>}
</div>
</div>
</div>
</MediaStreamControllerAPIProvider>
);

Full Component Code

// PrivateBroadcaster.tsx

/**
* PrivateBroadcaster Component
*
* ADVANCED IMPLEMENTATION: Private Calls (Bidirectional)
*
* This component enables bidirectional streaming where a private broadcaster can accept
* private viewer requests to join the call and stream back to the private broadcaster. This creates
* a 1:1 interactive connection, different from standard broadcasting where viewers
* only receive the stream.
*
* KEY CONCEPTS:
* 1. BIDIRECTIONAL STREAMING: Both private broadcaster and private viewer see each other
* 2. CALL REQUEST FLOW: Private viewer requests → Private broadcaster accepts/denies → Private viewer streams back
* 3. STREAM EVENT HANDLING: React to streamAdded/streamRemoved events
* 4. VIEWER MANAGEMENT: Accept, deny, and kick viewers
*
* USE CASES:
* - 1:1 consultations (doctor-patient, tutor-student)
* - Interactive Q&A sessions
* - Live customer support with video
* - Interview or audition streams
* - Any scenario requiring broadcaster-viewer interaction
*
* ARCHITECTURE:
* - Left panel: Broadcaster's preview player (own stream + call controls)
* - Right panel: Viewer's player (when accepted) + call request UI
*
* PREREQUISITES:
* Assumes familiarity with:
* - Preview Player setup and broadcast management
* - Player components and manifest playback
* - Call event handling
* - Authentication and call creation
*/

import React, { useState, useEffect, useCallback } from "react";
import { types, hooks, context } from "@video/video-client-react";

/**
* REUSABLE COMPONENTS
*
* - Preview Player: Broadcaster's video preview with device controls
* - CallRequested: UI for accepting/denying viewer call requests
* - useCallControls: Hook for managing call and broadcast state
* - Player: Displays viewer's incoming stream
*/
import PreviewPlayer from "../../components/PreviewPlayer";
import CallRequested from "./CallRequested";
import { useCallControls } from "../../components/CallControls";
import Player from "../../components/Player";

const { useAuthClient, usePreviewPlayer } = hooks;
const { MediaStreamControllerAPIProvider, PlayerAPIProvider } = context;

/**
* STYLING CONFIGURATION
*
*/
const classNames = {
wrapperClassName: "flex flex-row",
leftPanelClassName: "w-1/2",
rightPanelClassName: "w-1/2",
playerWrapperClassName: "w-full bg-gray-200",
callRequestedClassName: "w-full bg-gray-200",
viewerPlayerClassName: "w-full bg-gray-200",
kickViewerButtonClassName: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full cursor-pointer",
loadingScreenClassName: "w-full h-full bg-gray-200",
previewPlayerClassNames: {
wrapperClassName: "gap-4 flex flex-col",
videoContainerClassName: "w-full h-auto",
videoClassName: "w-full h-auto",
controlBarClassName: "flex flex-col",
controlBarItemClassName: "flex flex-col my-2 gap-2",
},
};

/**
* PROPS INTERFACE
*
* Standard broadcast props plus callbacks for parent component
*/
interface DirectStreamingBroadcasterProps {
backendEndpoint: string;
streamKey: string;
token: string;
cbCallId: (callId: string) => void;
cbBroadcast: (broadcast: types.BroadcastAPI | null) => void;
}

function DirectStreamingBroadcaster({ backendEndpoint, streamKey, token, cbCallId, cbBroadcast }: DirectStreamingBroadcasterProps): React.ReactElement | null {
/**
* STATE MANAGEMENT
*
* Two key pieces of state for direct streaming:
* 1. viewer: Stores the viewer's peer and stream information
* 2. callRequested: Tracks the call request lifecycle
*/
const [viewer, setViewer] = useState< types.CallEvents["streamAdded"] | null>(null);
const [callRequested, setCallRequested] = useState<"none" | "requested" | "accepted" | "denied">("none");
/**
* INITIALIZE BROADCASTER'S MEDIA
*
* Standard preview player setup - same as regular broadcasting
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});

/**
* AUTHENTICATION
*
* Create auth client with broadcaster scope
*/
const authClient = useAuthClient(token);


/**
* CALL AND BROADCAST SETUP
*
* Use the CallControls hook to manage call and broadcast state.
* This is the same pattern as standard broadcasting.
*/
const callOptions = {streamKey, user: {userId: "123", displayName: "John Doe"}, backendEndpoints: [backendEndpoint], auth: authClient};
const broadcastOptions = {streamName: "default"};
const { renderControls, call, broadcast, setCall, setBroadcast } = useCallControls({callOptions, broadcastOptions, type: "owner"});

/**
* (OPTIONAL) NOTIFY PARENT OF BROADCAST STATE
*/
useEffect(() => {
cbBroadcast(broadcast);
}, [broadcast, cbBroadcast]);

/**
* STREAM EVENT HANDLING (THE CORE LOGIC)
*
* This is where direct streaming differs from regular broadcasting.
* We listen for streamAdded and streamRemoved events to know when
* viewers join and leave.
*
* EVENT FLOW:
* 1. Viewer requests to join → streamAdded event fires
* 2. We set callRequested to "requested" → Shows accept/deny UI
* 3. Broadcaster accepts → callRequested set to "accepted" → Show viewer's stream
* 4. Viewer leaves or is kicked → streamRemoved event fires → Clean up
*
* IMPORTANT: This is a 1:1 connection pattern. For multiple viewers,
* you'd need to manage an array of viewers instead of a single viewer state.
*/
useEffect(() => {
const onStreamAdded = (ev: types.CallEvents["streamAdded"]): void => {
// A viewer's stream has been added to the call
setCallRequested("requested"); // Show accept/deny UI
setViewer(ev); // Store viewer info for later use
};

const onStreamRemoved = (ev: types.CallEvents["streamRemoved"]): void => {
// Viewer has left or been kicked
setViewer(null); // Clear viewer state
// Note: Player disposal is handled automatically by the Player component
};

if (call != null) {
// Attach event listeners when call is active
call.on("streamAdded", onStreamAdded);
call.on("streamRemoved", onStreamRemoved);
}

// Notify parent of call ID
cbCallId(call?.id ?? "");

// Cleanup: Remove event listeners when call changes or component unmounts
return () => {
call?.off("streamAdded", onStreamAdded);
call?.off("streamRemoved", onStreamRemoved);
};
}, [call, cbCallId]);


/**
* VIEWER MANAGEMENT: KICK VIEWER
*
* Removes a viewer from the call. This is used both for denying
* initial requests and for kicking viewers during the call.
*
* The kickViewer method:
* - Removes the viewer from the call
* - Triggers streamRemoved event (handled in useEffect above)
* - Parameters:
* - userId: Viewer to kick
* - false: Don't ban (viewer can rejoin)
* - true: Force kick (disconnect immediately)
*/
const handleKickViewer = useCallback((): void => {
if (call == null || viewer?.peer == null || viewer.peer.userId == null) {
throw new Error("Unable to kick viewer: call or viewer not found");
}
call.kickViewer(viewer.peer.userId, false, true).catch((err) => {
throw new Error(`Unable to kick viewer: ${err instanceof Error ? err.message : String(err)}`);
});
}, [viewer, call]);

/**
* CLEANUP EFFECTS
*
* Proper cleanup is critical for direct streaming to avoid:
* - Memory leaks from undisposed players
* - Lingering WebRTC connections
* - Event listener leaks
*
* We dispose in separate effects to handle each resource independently.
*/
useEffect(() => {
return () => {
if (call == null) return;
call.dispose("Disposed by useEffect - call cleanup");
setCall(null);
setBroadcast(null);
};
}, [call, setCall, setBroadcast]);

useEffect(() => {
return () => {
if (mediaStreamController == null) return;
mediaStreamController.dispose("Disposed by useEffect - mediaStreamController cleanup");
};
}, [mediaStreamController]);

useEffect(() => {
return () => {
if (previewPlayer == null) return;
previewPlayer.dispose("Disposed by useEffect - previewPlayer cleanup");
};
}, [previewPlayer]);

/**
* LOADING STATE
*/
if (!mediaStreamController || !previewPlayer) {
return <div className={classNames.loadingScreenClassName} />;
}

/**
* RENDER: SPLIT PANEL LAYOUT
*
* LEFT PANEL: Broadcaster's view
* - Preview Player: Own video preview with device controls
* - Call Controls: Create call, start/stop broadcast
*
* RIGHT PANEL: Viewer interaction (conditional based on callRequested state)
* - "requested": Show accept/deny UI (CallRequested component)
* - "accepted": Show viewer's stream + kick button
* - "denied": Show denied message
* - "none": Empty (no viewer)
*
* STATE FLOW:
* none → requested (streamAdded fires) → accepted/denied (user choice) → none (streamRemoved fires)
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<div className={classNames.wrapperClassName}>
{/* Left Panel: Broadcaster's Preview Player */}
<div className={classNames.leftPanelClassName}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<PreviewPlayer classNames={classNames.previewPlayerClassNames}/>
{renderControls()}
</PlayerAPIProvider>
</div>

{/* Right Panel: Viewer Interaction */}
<div className={classNames.rightPanelClassName}>
<div className={classNames.callRequestedClassName}>
{/* State: Call Requested - Show Accept/Deny UI */}
{callRequested === "requested" &&
<CallRequested
setCallRequested={setCallRequested}
kickViewer={handleKickViewer}
/>
}

{/* State: Accepted - Show Viewer's Stream */}
{callRequested === "accepted" && viewer?.stream != null &&
<>
<Player source={viewer.stream} />
<button onClick={handleKickViewer} className={classNames.kickViewerButtonClassName}>
Kick Viewer
</button>
</>
}

{/* State: Denied - Show Denied Message */}
{callRequested === "denied" && <div>Call denied</div>}
</div>
</div>
</div>
</MediaStreamControllerAPIProvider>
);
}
export default DirectStreamingBroadcaster;

Full Code

Private Broadcaster Components

// CallRequested.tsx

/**
* CallRequested Component
*
* OVERVIEW:
* A simple UI component that displays when a viewer requests to join
* a direct streaming session. Provides accept/deny controls for the broadcaster
* to manage incoming call requests.
*
* PURPOSE:
* This component bridges the gap between receiving a streamAdded event
* and making the decision to allow the viewer into the broadcast. It gives
* the broadcaster control over who can join their direct streaming session.
*
* INTEGRATION:
* Used by DirectStreamingBroadcaster when callRequested state is "requested".
* The broadcaster component manages the state and provides the callbacks.
*
* USER FLOW:
* 1. Viewer initiates call request → streamAdded event fires
* 2. DirectStreamingBroadcaster shows this component
* 3. Broadcaster clicks Accept → callRequested set to "accepted" → viewer's stream displays
* 4. OR Broadcaster clicks Deny → viewer kicked → callRequested set to "denied"
*
* CUSTOMIZATION:
* This is an example implementation. In production, you might want to:
* - Show viewer information (name, avatar)
* - Add "block" functionality
* - Include a timeout for auto-denial
* - Show multiple pending requests
* - Add request reason/message
*/

import React, { useCallback } from "react";

/**
* STYLING CONFIGURATION
*
*/
const classNames = {
wrapperClassName: "flex items-center justify-center bg-black/50",
containerClassName: "relative w-96 rounded-lg bg-white p-6 shadow-lg",
titleClassName: "mb-4 text-xl font-bold",
descriptionClassName: "mb-6 text-gray-600",
buttonContainerClassName: "flex justify-end space-x-4",
acceptButtonClassName: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full cursor-pointer",
denyButtonClassName: "bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-full cursor-pointer",
};

/**
* PROPS INTERFACE
*
* Both props are provided by the PrivateBroadcaster parent component.
*/
interface CallRequestedProps {
/**
* Callback to update the call request state in the parent component
*
* States:
* - "none": No request pending
* - "requested": Viewer has requested to join (this component is shown)
* - "accepted": Broadcaster accepted (viewer's stream will display)
* - "denied": Broadcaster denied (viewer was kicked)
*/
setCallRequested: (callRequested: "none" | "requested" | "accepted" | "denied") => void;

/**
* Callback to kick the viewer from the call
*
* This is the kickViewer function from PrivateBroadcaster
* which calls call.kickViewer() with the viewer's userId.
*/
kickViewer: () => void;
}


function CallRequested({ setCallRequested, kickViewer }: CallRequestedProps): JSX.Element | null {

/**
* DENY HANDLER
*
* When broadcaster denies the call request:
* 1. Kick the viewer from the call (removes their stream)
* 2. Update state to "denied" (shows denied message in parent)
*
* The kickViewer call triggers:
* - call.kickViewer(userId, false, true)
* - streamRemoved event in parent component
* - Viewer is disconnected from the call
*/
const handleDenyCall = useCallback(() => {
kickViewer();
setCallRequested("denied");
}, [kickViewer, setCallRequested]);
/**
* RENDER: CALL REQUEST MODAL
*
* Simple UI with:
* - Overlay background (semi-transparent black)
* - Centered card with title and description
* - Two action buttons:
* - Accept: Sets state to "accepted" (parent will display viewer's stream)
* - Deny: Kicks viewer and sets state to "denied"
*
* ACCEPT FLOW:
* Clicking Accept sets callRequested to "accepted" which causes the parent
* component to render the Player with the viewer's stream.
*
* DENY FLOW:
* Clicking Deny kicks the viewer (disconnects them) and shows a "denied" message
* in the parent component.
*/
return (
<div className={classNames.wrapperClassName}>
<div className={classNames.containerClassName}>
<h2 className={classNames.titleClassName}>Incoming Call</h2>
<p className={classNames.descriptionClassName}>Someone is requesting to start a call with you.</p>
<div className={classNames.buttonContainerClassName}>
<button
type="button"
onClick={() => setCallRequested("accepted")}
className={classNames.acceptButtonClassName}
>Accept</button>
<button
type="button"
onClick={handleDenyCall}
className={classNames.denyButtonClassName}
>Deny</button>
</div>
</div>
</div>
);
}

export default CallRequested;
// PrivateBroadcaster.tsx

/**
* PrivateBroadcaster Component
*
* ADVANCED IMPLEMENTATION: Private Calls (Bidirectional)
*
* This component enables bidirectional streaming where a private broadcaster can accept
* private viewer requests to join the call and stream back to the private broadcaster. This creates
* a 1:1 interactive connection, different from standard broadcasting where viewers
* only receive the stream.
*
* KEY CONCEPTS:
* 1. BIDIRECTIONAL STREAMING: Both private broadcaster and private viewer see each other
* 2. CALL REQUEST FLOW: Private viewer requests → Private broadcaster accepts/denies → Private viewer streams back
* 3. STREAM EVENT HANDLING: React to streamAdded/streamRemoved events
* 4. VIEWER MANAGEMENT: Accept, deny, and kick viewers
*
* USE CASES:
* - 1:1 consultations (doctor-patient, tutor-student)
* - Interactive Q&A sessions
* - Live customer support with video
* - Interview or audition streams
* - Any scenario requiring broadcaster-viewer interaction
*
* ARCHITECTURE:
* - Left panel: Broadcaster's preview player (own stream + call controls)
* - Right panel: Viewer's player (when accepted) + call request UI
*
* PREREQUISITES:
* Assumes familiarity with:
* - Preview Player setup and broadcast management
* - Player components and manifest playback
* - Call event handling
* - Authentication and call creation
*/

import React, { useState, useEffect, useCallback } from "react";
import { types, hooks, context } from "@video/video-client-react";

/**
* REUSABLE COMPONENTS
*
* - Preview Player: Broadcaster's video preview with device controls
* - CallRequested: UI for accepting/denying viewer call requests
* - useCallControls: Hook for managing call and broadcast state
* - Player: Displays viewer's incoming stream
*/
import PreviewPlayer from "../../components/PreviewPlayer";
import CallRequested from "./CallRequested";
import { useCallControls } from "../../components/CallControls";
import Player from "../../components/Player";

const { useAuthClient, usePreviewPlayer } = hooks;
const { MediaStreamControllerAPIProvider, PlayerAPIProvider } = context;

/**
* STYLING CONFIGURATION
*
*/
const classNames = {
wrapperClassName: "flex flex-row",
leftPanelClassName: "w-1/2",
rightPanelClassName: "w-1/2",
playerWrapperClassName: "w-full bg-gray-200",
callRequestedClassName: "w-full bg-gray-200",
viewerPlayerClassName: "w-full bg-gray-200",
kickViewerButtonClassName: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full cursor-pointer",
loadingScreenClassName: "w-full h-full bg-gray-200",
previewPlayerClassNames: {
wrapperClassName: "gap-4 flex flex-col",
videoContainerClassName: "w-full h-auto",
videoClassName: "w-full h-auto",
controlBarClassName: "flex flex-col",
controlBarItemClassName: "flex flex-col my-2 gap-2",
},
};

/**
* PROPS INTERFACE
*
* Standard broadcast props plus callbacks for parent component
*/
interface DirectStreamingBroadcasterProps {
backendEndpoint: string;
streamKey: string;
token: string;
cbCallId: (callId: string) => void;
cbBroadcast: (broadcast: types.BroadcastAPI | null) => void;
}

function DirectStreamingBroadcaster({ backendEndpoint, streamKey, token, cbCallId, cbBroadcast }: DirectStreamingBroadcasterProps): React.ReactElement | null {
/**
* STATE MANAGEMENT
*
* Two key pieces of state for direct streaming:
* 1. viewer: Stores the viewer's peer and stream information
* 2. callRequested: Tracks the call request lifecycle
*/
const [viewer, setViewer] = useState< types.CallEvents["streamAdded"] | null>(null);
const [callRequested, setCallRequested] = useState<"none" | "requested" | "accepted" | "denied">("none");
/**
* INITIALIZE BROADCASTER'S MEDIA
*
* Standard preview player setup - same as regular broadcasting
*/
const { mediaStreamController, previewPlayer } = usePreviewPlayer({});

/**
* AUTHENTICATION
*
* Create auth client with broadcaster scope
*/
const authClient = useAuthClient(token);


/**
* CALL AND BROADCAST SETUP
*
* Use the CallControls hook to manage call and broadcast state.
* This is the same pattern as standard broadcasting.
*/
const callOptions = {streamKey, user: {userId: "123", displayName: "John Doe"}, backendEndpoints: [backendEndpoint], auth: authClient};
const broadcastOptions = {streamName: "default"};
const { renderControls, call, broadcast, setCall, setBroadcast } = useCallControls({callOptions, broadcastOptions, type: "owner"});

/**
* (OPTIONAL) NOTIFY PARENT OF BROADCAST STATE
*/
useEffect(() => {
cbBroadcast(broadcast);
}, [broadcast, cbBroadcast]);

/**
* STREAM EVENT HANDLING (THE CORE LOGIC)
*
* This is where direct streaming differs from regular broadcasting.
* We listen for streamAdded and streamRemoved events to know when
* viewers join and leave.
*
* EVENT FLOW:
* 1. Viewer requests to join → streamAdded event fires
* 2. We set callRequested to "requested" → Shows accept/deny UI
* 3. Broadcaster accepts → callRequested set to "accepted" → Show viewer's stream
* 4. Viewer leaves or is kicked → streamRemoved event fires → Clean up
*
* IMPORTANT: This is a 1:1 connection pattern. For multiple viewers,
* you'd need to manage an array of viewers instead of a single viewer state.
*/
useEffect(() => {
const onStreamAdded = (ev: types.CallEvents["streamAdded"]): void => {
// A viewer's stream has been added to the call
setCallRequested("requested"); // Show accept/deny UI
setViewer(ev); // Store viewer info for later use
};

const onStreamRemoved = (ev: types.CallEvents["streamRemoved"]): void => {
// Viewer has left or been kicked
setViewer(null); // Clear viewer state
// Note: Player disposal is handled automatically by the Player component
};

if (call != null) {
// Attach event listeners when call is active
call.on("streamAdded", onStreamAdded);
call.on("streamRemoved", onStreamRemoved);
}

// Notify parent of call ID
cbCallId(call?.id ?? "");

// Cleanup: Remove event listeners when call changes or component unmounts
return () => {
call?.off("streamAdded", onStreamAdded);
call?.off("streamRemoved", onStreamRemoved);
};
}, [call, cbCallId]);


/**
* VIEWER MANAGEMENT: KICK VIEWER
*
* Removes a viewer from the call. This is used both for denying
* initial requests and for kicking viewers during the call.
*
* The kickViewer method:
* - Removes the viewer from the call
* - Triggers streamRemoved event (handled in useEffect above)
* - Parameters:
* - userId: Viewer to kick
* - false: Don't ban (viewer can rejoin)
* - true: Force kick (disconnect immediately)
*/
const handleKickViewer = useCallback((): void => {
if (call == null || viewer?.peer == null || viewer.peer.userId == null) {
throw new Error("Unable to kick viewer: call or viewer not found");
}
call.kickViewer(viewer.peer.userId, false, true).catch((err) => {
throw new Error(`Unable to kick viewer: ${err instanceof Error ? err.message : String(err)}`);
});
}, [viewer, call]);

/**
* CLEANUP EFFECTS
*
* Proper cleanup is critical for direct streaming to avoid:
* - Memory leaks from undisposed players
* - Lingering WebRTC connections
* - Event listener leaks
*
* We dispose in separate effects to handle each resource independently.
*/
useEffect(() => {
return () => {
if (call == null) return;
call.dispose("Disposed by useEffect - call cleanup");
setCall(null);
setBroadcast(null);
};
}, [call, setCall, setBroadcast]);

useEffect(() => {
return () => {
if (mediaStreamController == null) return;
mediaStreamController.dispose("Disposed by useEffect - mediaStreamController cleanup");
};
}, [mediaStreamController]);

useEffect(() => {
return () => {
if (previewPlayer == null) return;
previewPlayer.dispose("Disposed by useEffect - previewPlayer cleanup");
};
}, [previewPlayer]);

/**
* LOADING STATE
*/
if (!mediaStreamController || !previewPlayer) {
return <div className={classNames.loadingScreenClassName} />;
}

/**
* RENDER: SPLIT PANEL LAYOUT
*
* LEFT PANEL: Broadcaster's view
* - Preview Player: Own video preview with device controls
* - Call Controls: Create call, start/stop broadcast
*
* RIGHT PANEL: Viewer interaction (conditional based on callRequested state)
* - "requested": Show accept/deny UI (CallRequested component)
* - "accepted": Show viewer's stream + kick button
* - "denied": Show denied message
* - "none": Empty (no viewer)
*
* STATE FLOW:
* none → requested (streamAdded fires) → accepted/denied (user choice) → none (streamRemoved fires)
*/
return (
<MediaStreamControllerAPIProvider mediaStreamControllerAPI={mediaStreamController}>
<div className={classNames.wrapperClassName}>
{/* Left Panel: Broadcaster's Preview Player */}
<div className={classNames.leftPanelClassName}>
<PlayerAPIProvider playerAPI={previewPlayer}>
<PreviewPlayer classNames={classNames.previewPlayerClassNames}/>
{renderControls()}
</PlayerAPIProvider>
</div>

{/* Right Panel: Viewer Interaction */}
<div className={classNames.rightPanelClassName}>
<div className={classNames.callRequestedClassName}>
{/* State: Call Requested - Show Accept/Deny UI */}
{callRequested === "requested" &&
<CallRequested
setCallRequested={setCallRequested}
kickViewer={handleKickViewer}
/>
}

{/* State: Accepted - Show Viewer's Stream */}
{callRequested === "accepted" && viewer?.stream != null &&
<>
<Player source={viewer.stream} />
<button onClick={handleKickViewer} className={classNames.kickViewerButtonClassName}>
Kick Viewer
</button>
</>
}

{/* State: Denied - Show Denied Message */}
{callRequested === "denied" && <div>Call denied</div>}
</div>
</div>
</div>
</MediaStreamControllerAPIProvider>
);
}
export default DirectStreamingBroadcaster;

Supporting Components

The following components are used by the Private Broadcaster Component and are documented in Set Up A Livestream Video:

// PreviewPlayer.tsx

/**
* Preview Player Component
*
* OVERVIEW:
* A reusable preview player component that displays a video preview and
* provides standard device controls. This component is designed to be
* dropped into any video broadcasting application with minimal configuration.
*
* WHAT IT DOES:
* - Displays video preview (what your camera sees)
* - Provides toggle buttons for camera and microphone
* - Offers device selection dropdowns (which camera, which mic)
* - Allows resolution/quality selection
* - Automatically connects to video client context
*
* KEY FEATURES:
* - Fully self-contained: No state management needed in parent
* - Customizable styling via className props
* - Responsive layout (adapts to screen size)
* - Accessibility-friendly controls
* - Built-in error handling
*
* REQUIREMENTS:
* Must be used inside:
* - MediaStreamControllerAPIProvider (for device control)
* - PlayerAPIProvider (for video display)
*
* @example
* ```tsx
* <MediaStreamControllerAPIProvider mediaStreamControllerAPI={msc}>
* <PlayerAPIProvider playerAPI={player}>
* <Encoder />
* </PlayerAPIProvider>
* </MediaStreamControllerAPIProvider>
* ```
*
* @example With custom styling:
* ```tsx
* <Encoder
* classNames={{
* wrapperClassName: "my-custom-layout",
* videoClassName: "rounded-lg shadow-lg"
* }}
* />
* ```
*/

import React, { memo, useRef } from "react";
import {components, } from "@video/video-client-react";

/**
* EXTRACT UI COMPONENTS
*
* These are pre-built components from @video/video-client-react:
*
* - Video: Displays the video stream
* - AudioSourceSelect: Dropdown to select microphone
* - VideoSourceSelect: Dropdown to select camera
* - ResolutionSelect: Dropdown to select video quality
* - ToggleMicButton: Button to mute/unmute microphone
* - ToggleCameraButton: Button to enable/disable camera
*
* All these components automatically connect to the video client
* context, so they "just work" without prop drilling.
*/
const { Video, AudioSourceSelect, VideoSourceSelect, ResolutionSelect, ToggleMicButton, ToggleCameraButton } = components;

/**
* STYLING INTERFACE
*
* */
interface PreviewPlayerClassNames {
wrapperClassName: string;
videoContainerClassName: string;
videoClassName: string;
controlBarClassName: string;
cntrolBarItemClassName: string;
}

/**
* DEFAULT STYLING
*
* Default styling using Tailwind CSS classes.
*/
const defaultClassNames = {
wrapperClassName: "gap-4 flex flex-row",
videoContainerClassName: "md:w-1/2",
videoClassName: "w-full h-auto",
controlBarClassName: "flex flex-col",
controlBarItemClassName: "flex flex-col my-2 gap-2",
};

/**
* PROPS INTERFACE
*
*/
interface PreviewPlayerProps {
/** Optional custom class names (partial override of defaults) */
classNames?: Partial<PreviewPlayerClassNames>;
}

function PreviewPlayer({classNames}: PreviewPlayerProps): JSX.Element {

const mergedClassNames = { ...defaultClassNames,...classNames };

/**
* VIDEO ELEMENT REF
*
* This is a required prop for the Video component.
*/
const videoElement = useRef<HTMLVideoElement>(null);

/**
* RENDER THE PREVIEW PLAYER UI
*
*/
return (
<div className={mergedClassNames.wrapperClassName}>
{/* VIDEO PREVIEW SECTION */}
<div className={mergedClassNames.videoContainerClassName}>
{/*
The Video component displays what your camera sees.
It automatically connects to the PlayerAPI from context.
*/}
<Video ref={videoElement} className={mergedClassNames.videoClassName} />
</div>

{/* CONTROLS SECTION */}
<div className={mergedClassNames.controlBarClassName}>
{/*
Toggle Controls Group
Quick on/off buttons for camera and microphone
*/}
<div className={mergedClassNames.controlBarItemClassName}>
<ToggleMicButton />
<ToggleCameraButton />
</div>

{/*
Device Selection Group
Dropdowns for choosing which camera/mic and quality
*/}
<div className={mergedClassNames.controlBarItemClassName}>
<AudioSourceSelect />
<VideoSourceSelect />
<ResolutionSelect />
</div>
</div>
</div>
);
}

/**
* EXPORT WITH MEMOIZATION
*
*/
export default memo(PreviewPlayer);
// CallControls.tsx

/**
* CallControls Component & Hook
*
* OVERVIEW:
* This file provides both a component and a custom hook for managing
* call and broadcast state with automatic UI controls. It's a powerful
* abstraction that handles all the complexity of call/broadcast lifecycle.
*
* WHAT IT DOES FOR YOU:
* - Manages call and broadcast state automatically
* - Renders the right buttons based on current state
* - Handles state transitions (no call → call → broadcasting)
* - Provides callbacks for cleanup and notifications
* - Supports multiple usage patterns (owner, participant, broadcast-only)
*
* KEY BENEFITS:
* - No need to manually track call/broadcast state
* - No need to conditionally render buttons yourself
* - No need to write call creation/disposal logic
* - Consistent UX across your application
* - Easy to extend and customize
*
* USAGE PATTERNS:
* 1. "owner": Creates and hosts calls (shows CreateCallButton)
* 2. "participant": Joins existing calls (shows JoinCallButton)
* 3. "broadcast-controls-only": Only manages broadcast (requires existing call)
*
* @example As a hook:
* ```tsx
* const { renderControls, call, broadcast } = useCallControls({
* callOptions: { streamKey, auth, ... },
* broadcastOptions: { streamName: 'default' },
* type: 'owner'
* });
*
* return <div>{renderControls()}</div>;
* ```
*
* @example As a component:
* ```tsx
* <CallControls
* callOptions={{ streamKey, auth, ... }}
* broadcastOptions={{ streamName: 'default' }}
* type="owner"
* />
* ```
*/

import React, { useState } from "react";
import { components, types, context } from "@video/video-client-react";

/**
* EXTRACT PRE-BUILT BUTTON COMPONENTS
*
* These components handle button UI and click logic:
* - CreateCallButton: Creates a new call
* - JoinCallButton: Joins an existing call by ID
* - EndCallButton: Ends the current call
* - StartBroadcastButton: Starts broadcasting to a call
* - EndBroadcastButton: Stops broadcasting
*
* All buttons automatically handle loading states, errors, and cleanup.
*/
const { CreateCallButton, EndCallButton, StartBroadcastButton, EndBroadcastButton, JoinCallButton } = components;

/**
* EXTRACT CONTEXT PROVIDERS
*
* These make call and broadcast instances available to button components:
* - CallAPIProvider: Shares call instance with children
* - BroadcastAPIProvider: Shares broadcast instance with children
*/
const { CallAPIProvider, BroadcastAPIProvider } = context;


interface CallControlsOptions {
callOptions?: types.CallOptions;
broadcastOptions: types.BroadcastOptions;
type: 'owner' | 'participant' | 'broadcast-controls-only';
call?: types.CallAPI | null;
}

type CallState = 'no-call-owner' | 'no-call-participant' | 'call-no-broadcast' | 'call-with-broadcast' | 'start-broadcast-only' | 'end-broadcast-only';

interface CallControlsReturn {
call: types.CallAPI | null;
broadcast: types.BroadcastAPI | null;
setCall: (call: types.CallAPI | null) => void;
setBroadcast: (broadcast: types.BroadcastAPI | null) => void;
state: CallState;
renderControls: () => React.ReactElement;
}


/**
* Utility function to determine the current call/broadcast state
*/
function getCallState(call: types.CallAPI | null, broadcast: types.BroadcastAPI | null, type: 'owner' | 'participant' | 'broadcast-controls-only'): CallState {
switch (type) {
case 'owner':
if (call == null) {
return 'no-call-owner';
}
if (broadcast == null) {
return 'call-no-broadcast';
}
return 'call-with-broadcast';

case 'participant':
if (call == null) {
return 'no-call-participant';
}
if (broadcast == null) {
return 'call-no-broadcast';
}
return 'call-with-broadcast';

case 'broadcast-controls-only':
if (call == null) {
throw new Error('Call is required when type is broadcast-controls-only');
}
if (broadcast == null) {
return 'start-broadcast-only';
}
return 'end-broadcast-only';

default:
// This should never happen if TypeScript types are correct
const _exhaustive: never = type;
throw new Error(`Unknown type: ${_exhaustive}`);
}
}

/**
* Custom hook for managing broadcaster call and broadcast state with controls
*
* @param options - Call and broadcast configuration options
* @returns Object containing state, setters, and a render function for controls
*
* @example
* ```tsx
* function MyComponent() {
* const { renderControls } = useBroadcasterCallControls({
* callOptions: { streamKey: '...', auth: authClient, ... },
* broadcastOptions: { streamName: 'default' }
* });
*
* return <div>{renderControls()}</div>;
* }
* ```
*/
function useCallControls(options: CallControlsOptions): CallControlsReturn {
const { callOptions, broadcastOptions } = options;


const [call, setCall] = useState<types.CallAPI | null>(options.call ?? null);
const [broadcast, setBroadcast] = useState<types.BroadcastAPI | null>(null);

const state = getCallState(call, broadcast, options.type);



/**
* Renders the appropriate controls based on current call/broadcast state
*/
const renderControls = (): React.ReactElement => {
switch (state) {
case 'no-call-owner':
if (callOptions == null) {
throw new Error('Call options are required');
}
// No active call - show CreateCallButton
return (
<CreateCallButton
callOptions={callOptions}
setCall={setCall}
/>
);

case 'no-call-participant':
if (callOptions == null || callOptions.callId == null) {
throw new Error('Call options and call ID are required');
}
// No active call - show JoinCallButton
return <JoinCallButton callId={callOptions.callId} joinCallOptions={callOptions} setCall={setCall} />;
case 'call-no-broadcast':
// Call active, no broadcast - show EndCallButton and StartBroadcastButton
return (
<CallAPIProvider callAPI={call!}>
<EndCallButton onDisposed={() => setCall(null)} />
<StartBroadcastButton
broadcastOptions={broadcastOptions}
setBroadcast={setBroadcast}
/>
</CallAPIProvider>
);

case 'call-with-broadcast':
// Call and broadcast active - show EndCallButton and EndBroadcastButton
return (

<CallAPIProvider callAPI={call!}>
<EndCallButton onDisposed={() => setCall(null)} />
<BroadcastAPIProvider broadcastAPI={broadcast!}>
<EndBroadcastButton onDisposed={() => setBroadcast(null)} />
</BroadcastAPIProvider>
</CallAPIProvider>

);

case 'start-broadcast-only':
return (
<CallAPIProvider callAPI={call!}>
<StartBroadcastButton broadcastOptions={broadcastOptions} setBroadcast={setBroadcast} />
</CallAPIProvider>
);

case 'end-broadcast-only':
return (
<CallAPIProvider callAPI={call!}>
<BroadcastAPIProvider broadcastAPI={broadcast!}>
<EndBroadcastButton onDisposed={() => setBroadcast(null)} />
</BroadcastAPIProvider>
</CallAPIProvider>

);

default:
// Exhaustive check - TypeScript will error if we miss a case
const _exhaustive: CallState = state;
return _exhaustive;
}
};


return {
call,
broadcast,
setCall,
setBroadcast,
state,
renderControls,
};
}

/**
* Component wrapper for the broadcaster call controls hook
* Manages call and broadcast state internally
*/
function CallControls(options: CallControlsOptions): React.ReactElement {
const { renderControls } = useCallControls(options);
return renderControls();
}


export default CallControls;
export { useCallControls, getCallState };
export type { CallControlsOptions, CallControlsReturn, CallState };

Key Concepts

Bidirectional Streaming

Bidirectional streaming enables two-way video communication:

  • Broadcaster creates call: Via createCall() with broadcaster options
  • Broadcaster broadcasts: Their camera/mic stream via call.broadcast()
  • Viewer joins call: Via player's WebRTC driver (automatically)
  • Viewer broadcasts back: Their camera/mic stream via call.broadcast()
  • 1:1 connection: Both parties interact in real-time
  • Single call: All communication happens through one WebRTC call

streamAdded Event

The streamAdded event is crucial for direct streaming:

  • Fired by: Call when a viewer joins and starts broadcasting
  • Provides: The viewer's media stream
  • Enables: Broadcaster to display viewer's video
  • Different from: Creating a player for a manifest (passive viewing)

Call vs Broadcast

Understanding the distinction:

  • Call: The connection to the Native Frame backend (created by broadcaster)
  • Broadcast: Sending media through the call (broadcaster's camera/mic to viewers)
  • Broadcaster flow: Create call → Start broadcast → Listen for viewer streams

Event Handling

The broadcaster responds to viewer actions:

streamAdded:

  • Viewer joins the call and starts broadcasting
  • Broadcaster creates player for viewer's stream
  • Triggers bidirectional streaming

streamRemoved:

  • Viewer disconnects or is kicked
  • Broadcaster disposes of viewer's player
  • Triggers cleanup

Authentication Scope

Direct Streaming Broadcaster:

  • Requires "broadcaster" scope
  • Can create calls and broadcasts
  • Can receive viewer streams
  • Can kick viewers from call

Direct Streaming Viewer:

  • Uses "private-viewer" scope
  • Can join call and broadcast back
  • Different from "viewer" scope (which cannot broadcast)

Basic Broadcaster:

  • Uses "broadcaster" scope
  • Creates calls and broadcasts
  • Typically doesn't receive viewer streams (one-way)

Direct Streaming Broadcaster vs Other Patterns

Direct Streaming Broadcaster:

  • Creates call with broadcaster options
  • Broadcasts their stream
  • Listens for streamAdded event
  • Creates player for incoming viewer streams
  • Bidirectional communication
  • 1:1 interactive session

Basic Broadcaster:

  • Creates call and broadcasts
  • Does not listen for viewer streams
  • One-way communication
  • Multiple viewers can watch

Direct Streaming Viewer:

  • Joins call via manifest player with WebRTC driver
  • Automatically joins call via joinedCall event
  • Sets up encoder to stream back
  • Bidirectional communication
  • 1:1 interactive session

Next Steps

To learn more about advanced streaming concepts: