Add basic screen sharing

This commit is contained in:
mplorentz
2026-03-26 10:59:50 -04:00
parent 5792f77fdc
commit ef291006e2
4 changed files with 137 additions and 27 deletions
+73 -13
View File
@@ -4,11 +4,12 @@
*/
import {
DisconnectReason,
LocalParticipant,
LocalTrackPublication,
Room as LiveKitRoom,
RoomEvent,
Track,
type AudioCaptureOptions,
type LocalParticipant,
} from "livekit-client"
import {derived, get, writable} from "svelte/store"
import {map, removeUndefined, uniqBy} from "@welshman/lib"
@@ -30,6 +31,7 @@ export type VoiceSession = {
room: LiveKitRoom
muted: boolean
cameraOn: boolean
screenShareOn: boolean
}
export type Pubkey = string
@@ -220,6 +222,18 @@ const onParticipantDisconnected = (participant: {identity: string}) => {
deleteParticipant(participant.identity)
}
const onLocalTrackUnpublished = (
publication: LocalTrackPublication,
participant: LocalParticipant,
) => {
if (publication.source !== Track.Source.ScreenShare) return
const session = get(currentVoiceSession)
if (!session || participant.identity !== session.room.localParticipant.identity) return
if (!session.screenShareOn) return
currentVoiceSession.set({...session, screenShareOn: false})
bumpVideoCallLayoutRevision()
}
let joinAbortController: AbortController | undefined
export const cancelJoinVoiceRoom = () => {
@@ -257,6 +271,7 @@ export const joinVoiceRoom = async (
liveKitRoom.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected)
liveKitRoom.on(RoomEvent.TrackSubscribed, onTrackSubscribed)
liveKitRoom.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
liveKitRoom.on(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished)
liveKitRoom.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
try {
@@ -280,7 +295,14 @@ export const joinVoiceRoom = async (
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
currentVoiceSession.set({url, h, room: liveKitRoom, muted, cameraOn: false})
currentVoiceSession.set({
url,
h,
room: liveKitRoom,
muted,
cameraOn: false,
screenShareOn: false,
})
voiceState.set(VoiceState.Connected)
playJoinSound()
} catch (e) {
@@ -307,6 +329,14 @@ export const leaveVoiceRoom = async () => {
}
}
if (session.screenShareOn) {
try {
await session.room.localParticipant.setScreenShareEnabled(false)
} catch {
/* pass */
}
}
voiceState.set(VoiceState.Disconnected)
videoCallLayoutRevision.set(0)
currentVoiceSession.set(undefined)
@@ -341,26 +371,29 @@ export const toggleMute = async () => {
}
}
const roomHasSubscribedRemoteCamera = (room: LiveKitRoom): boolean => {
const VISUAL_SOURCES = [Track.Source.Camera, Track.Source.ScreenShare] as const
const roomHasSubscribedRemoteVisual = (room: LiveKitRoom): boolean => {
for (const p of room.remoteParticipants.values()) {
const pub = p.getTrackPublication(Track.Source.Camera)
if (pub?.isSubscribed && pub.track) return true
for (const source of VISUAL_SOURCES) {
const pub = p.getTrackPublication(source)
if (pub?.isSubscribed && pub.track) return true
}
}
return false
}
/** True when the connected session has local camera on or any subscribed remote camera track. */
/** True when local camera/screen share is on or any subscribed remote camera/screen track. */
export const videoCallContentActive = derived(
[currentVoiceSession, voiceState, videoCallLayoutRevision],
([$session, $state, _rev]) => {
if ($state !== VoiceState.Connected || !$session) return false
if ($session.cameraOn) return true
return roomHasSubscribedRemoteCamera($session.room)
if ($session.cameraOn || $session.screenShareOn) return true
return roomHasSubscribedRemoteVisual($session.room)
},
)
/** Live camera tracks (local + remote) for layout automation. */
const countLiveCameraFeeds = (session: VoiceSession): number => {
const countLiveVisualFeeds = (session: VoiceSession): number => {
const room = session.room
let n = 0
const lp = room.localParticipant
@@ -368,9 +401,15 @@ const countLiveCameraFeeds = (session: VoiceSession): number => {
const pub = lp.getTrackPublication(Track.Source.Camera)
if (pub?.track) n += 1
}
if (session.screenShareOn) {
const pub = lp.getTrackPublication(Track.Source.ScreenShare)
if (pub?.track) n += 1
}
for (const rp of room.remoteParticipants.values()) {
const pub = rp.getTrackPublication(Track.Source.Camera)
if (pub?.isSubscribed && pub.track) n += 1
for (const source of VISUAL_SOURCES) {
const pub = rp.getTrackPublication(source)
if (pub?.isSubscribed && pub.track) n += 1
}
}
return n
}
@@ -379,7 +418,7 @@ export const videoTileCount = derived(
[currentVoiceSession, voiceState, videoCallLayoutRevision],
([$session, $state, _rev]) => {
if ($state !== VoiceState.Connected || !$session) return 0
return countLiveCameraFeeds($session)
return countLiveVisualFeeds($session)
},
)
@@ -403,3 +442,24 @@ export const toggleCamera = async () => {
pushToast({theme: "error", message: "Could not access camera"})
}
}
export const toggleScreenShare = async () => {
const session = get(currentVoiceSession)
if (!session) return
const screenShareOn = !session.screenShareOn
if (!screenShareOn) {
session.room.localParticipant.setScreenShareEnabled(false)
currentVoiceSession.set({...session, screenShareOn})
bumpVideoCallLayoutRevision()
return
}
try {
await session.room.localParticipant.setScreenShareEnabled(true)
currentVoiceSession.set({...session, screenShareOn})
bumpVideoCallLayoutRevision()
} catch (e) {
pushToast({theme: "error", message: "Could not start screen sharing"})
}
}