@airsurfer09/web-handsfree
Version:
A React package for integrating Convai's AI-powered voice assistants with LiveKit for real-time audio/video conversations
198 lines • 7.65 kB
JavaScript
import { useCallback, useState, useEffect } from "react";
import { logger } from "../utils/logger";
/**
* Hook for managing screen sharing controls in LiveKit.
*
* Provides methods to enable/disable screen sharing, manage screen share state,
* and handle screen sharing with audio support.
*
* Features:
* - Includes current tab in screen share options (selfBrowserSurface: "include")
* - Supports surface switching during screen share
* - Supports system audio capture
*
* Note: By default, browsers exclude the current tab from screen share to prevent
* infinite recursion ("hall of mirrors" effect). This hook enables it by setting
* selfBrowserSurface: "include", but be aware this may cause visual feedback loops.
*
* @param {Room | null} room - LiveKit room instance
* @returns {ScreenShareControls} Object containing screen share control methods and state
*
* @example
* ```tsx
* function ScreenShareControls() {
* const screenShareControls = useScreenShare(room);
*
* return (
* <div>
* <button onClick={screenShareControls.toggleScreenShare}>
* {screenShareControls.isScreenShareEnabled ? 'Stop Sharing' : 'Share Screen'}
* </button>
* </div>
* );
* }
* ```
*/
export const useScreenShare = (room) => {
const [isScreenShareEnabled, setIsScreenShareEnabled] = useState(false);
const [isScreenShareActive, setIsScreenShareActive] = useState(false);
// Update screen share state when room changes
useEffect(() => {
if (!room) {
setIsScreenShareEnabled(false);
setIsScreenShareActive(false);
return;
}
const localParticipant = room.localParticipant;
// Set initial state
setIsScreenShareEnabled(localParticipant.isScreenShareEnabled);
setIsScreenShareActive(localParticipant.isScreenShareEnabled);
// Listen for screen share state changes
const handleTrackPublished = (track) => {
if (track.source === 'screen_share') {
setIsScreenShareActive(true);
logger.log("🖥️ Screen share started");
}
};
const handleTrackUnpublished = (track) => {
if (track.source === 'screen_share') {
setIsScreenShareActive(false);
logger.log("🖥️ Screen share stopped");
}
};
const handleTrackMuted = (track) => {
if (track.source === 'screen_share') {
setIsScreenShareActive(false);
logger.log("🖥️ Screen share muted");
}
};
const handleTrackUnmuted = (track) => {
if (track.source === 'screen_share') {
setIsScreenShareActive(true);
logger.log("🖥️ Screen share unmuted");
}
};
// Subscribe to events
localParticipant.on('trackPublished', handleTrackPublished);
localParticipant.on('trackUnpublished', handleTrackUnpublished);
localParticipant.on('trackMuted', handleTrackMuted);
localParticipant.on('trackUnmuted', handleTrackUnmuted);
return () => {
localParticipant.off('trackPublished', handleTrackPublished);
localParticipant.off('trackUnpublished', handleTrackUnpublished);
localParticipant.off('trackMuted', handleTrackMuted);
localParticipant.off('trackUnmuted', handleTrackUnmuted);
};
}, [room]);
// Enable screen share
const enableScreenShare = useCallback(async () => {
if (!room) {
logger.warn("Cannot enable screen share: no room connection");
return;
}
try {
// Use setScreenShareEnabled with options to include current tab
await room.localParticipant.setScreenShareEnabled(true, {
// @ts-ignore - selfBrowserSurface is a valid Chrome option but may not be in types
selfBrowserSurface: "include",
surfaceSwitching: "include",
systemAudio: "include",
});
setIsScreenShareEnabled(true);
logger.log("🖥️ Screen share enabled successfully");
}
catch (error) {
logger.error("Failed to enable screen share:", error);
throw error;
}
}, [room]);
// Disable screen share
const disableScreenShare = useCallback(async () => {
if (!room) {
logger.warn("Cannot disable screen share: no room connection");
return;
}
try {
await room.localParticipant.setScreenShareEnabled(false);
setIsScreenShareEnabled(false);
logger.log("🖥️ Screen share disabled successfully");
}
catch (error) {
logger.error("Failed to disable screen share:", error);
throw error;
}
}, [room]);
// Toggle screen share
const toggleScreenShare = useCallback(async () => {
if (isScreenShareEnabled) {
await disableScreenShare();
}
else {
await enableScreenShare();
}
}, [isScreenShareEnabled, enableScreenShare, disableScreenShare]);
// Enable screen share with audio
const enableScreenShareWithAudio = useCallback(async () => {
if (!room) {
logger.warn("Cannot enable screen share with audio: no room connection");
return;
}
try {
// Create screen tracks with audio enabled and include current tab
const tracks = await room.localParticipant.createScreenTracks({
audio: true,
// @ts-ignore - selfBrowserSurface is a valid Chrome option but may not be in types
selfBrowserSurface: "include",
surfaceSwitching: "include",
systemAudio: "include",
});
// Publish each track
tracks.forEach((track) => {
room.localParticipant.publishTrack(track);
});
setIsScreenShareEnabled(true);
logger.log("🖥️ Screen share with audio enabled successfully");
}
catch (error) {
logger.error("Failed to enable screen share with audio:", error);
throw error;
}
}, [room]);
// Get screen share tracks
const getScreenShareTracks = useCallback(async () => {
if (!room) {
logger.warn("Cannot get screen share tracks: no room connection");
return [];
}
try {
const localParticipant = room.localParticipant;
const screenShareTracks = [];
// Get all published tracks
localParticipant.trackPublications.forEach((publication) => {
if (publication.source === 'screen_share' && publication.track) {
screenShareTracks.push(publication.track);
}
});
logger.log("🖥️ Screen share tracks:", screenShareTracks);
return screenShareTracks;
}
catch (error) {
logger.error("Failed to get screen share tracks:", error);
throw error;
}
}, [room]);
return {
// Screen share state
isScreenShareEnabled,
isScreenShareActive,
// Screen share controls
enableScreenShare,
disableScreenShare,
toggleScreenShare,
// Screen share with audio
enableScreenShareWithAudio,
// Screen share settings
getScreenShareTracks,
};
};
//# sourceMappingURL=useScreenShare.js.map