From a1c788114648ae5b28d219006c9aa3334e3295d6 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Mon, 8 Sep 2025 10:20:24 -0400 Subject: [PATCH 001/113] feat: add ability to inject `room` into useParticipantTracks --- .../react/src/hooks/useParticipantTracks.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/react/src/hooks/useParticipantTracks.ts b/packages/react/src/hooks/useParticipantTracks.ts index 661ee7683..e2384e4bc 100644 --- a/packages/react/src/hooks/useParticipantTracks.ts +++ b/packages/react/src/hooks/useParticipantTracks.ts @@ -2,10 +2,15 @@ import * as React from 'react'; import type { TrackReference } from '@livekit/components-core'; import { participantTracksObservable } from '@livekit/components-core'; import { useObservableState } from './internal'; -import type { Track } from 'livekit-client'; +import type { Room, Track } from 'livekit-client'; import { useMaybeParticipantContext } from '../context'; import { useParticipants } from './useParticipants'; +type UseParticipantTracksOptions = { + participantIdentity?: string; + room?: Room; +}; + /** * `useParticipantTracks` is a custom React that allows you to get tracks of a specific participant only, by specifiying the participant's identity. * If the participant identity is not passed the hook will try to get the participant from a participant context. @@ -13,10 +18,19 @@ import { useParticipants } from './useParticipants'; */ export function useParticipantTracks( sources: Track.Source[], - participantIdentity?: string, + optionsOrParticipantIdentity: UseParticipantTracksOptions | UseParticipantTracksOptions["participantIdentity"] = {}, ): TrackReference[] { + let participantIdentity: UseParticipantTracksOptions["participantIdentity"]; + let room: UseParticipantTracksOptions["room"]; + if (typeof optionsOrParticipantIdentity === 'string') { + participantIdentity = optionsOrParticipantIdentity; + } else { + participantIdentity = optionsOrParticipantIdentity?.participantIdentity; + room = optionsOrParticipantIdentity?.room; + } + const participantContext = useMaybeParticipantContext(); - const participants = useParticipants({ updateOnlyOn: [] }); + const participants = useParticipants({ room, updateOnlyOn: [] }); const p = React.useMemo(() => { if (participantIdentity) { From 3ddafb989f733d585b0453af543b94be8d871c9c Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Tue, 9 Sep 2025 13:24:06 -0400 Subject: [PATCH 002/113] feat: add optional generics to TrackReference This allows functions to limit whether they just want to take in TrackReferences from a given source - ie, the VideoTrack could be made to only accept TrackReference. --- .../track-reference/track-reference.types.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/core/src/track-reference/track-reference.types.ts b/packages/core/src/track-reference/track-reference.types.ts index 94f6db5fc..8dc5fa57b 100644 --- a/packages/core/src/track-reference/track-reference.types.ts +++ b/packages/core/src/track-reference/track-reference.types.ts @@ -7,21 +7,24 @@ import type { Participant, Track, TrackPublication } from 'livekit-client'; // ## TrackReference Types /** @public */ -export type TrackReferencePlaceholder = { - participant: Participant; +export type TrackReferencePlaceholder = { + participant: P; publication?: never; - source: Track.Source; + source: TrackSource; }; /** @public */ -export type TrackReference = { - participant: Participant; +export type TrackReference = { + participant: P; publication: TrackPublication; - source: Track.Source; + source: TrackSource; }; /** @public */ -export type TrackReferenceOrPlaceholder = TrackReference | TrackReferencePlaceholder; +export type TrackReferenceOrPlaceholder< + TrackSource extends Track.Source = Track.Source, + P = Participant, +> = TrackReference | TrackReferencePlaceholder; // ### TrackReference Type Predicates /** @internal */ From cf69b81987e6ce319ac64a84db52f4e0bd54f221 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Tue, 9 Sep 2025 13:26:33 -0400 Subject: [PATCH 003/113] feat: use generic TrackReference in a few core observable functions Note that just the return values are changing, not the argument definitions in other spots, so this shouldn't be a backwards compatibility issue. --- packages/core/src/observables/track.ts | 34 ++++++++++++++++---------- packages/core/src/types.ts | 4 +++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/core/src/observables/track.ts b/packages/core/src/observables/track.ts index 0b5b895d5..677ee53b4 100644 --- a/packages/core/src/observables/track.ts +++ b/packages/core/src/observables/track.ts @@ -12,7 +12,7 @@ import { allParticipantRoomEvents, participantTrackEvents } from '../helper'; import { log } from '../logger'; import type { TrackReference } from '../track-reference'; import { observeRoomEvents } from './room'; -import type { ParticipantTrackIdentifier } from '../types'; +import type { ExtractTrackSourcesFromParticipantTrackIdentifier, ParticipantTrackIdentifier } from '../types'; import { observeParticipantEvents } from './participant'; // @ts-ignore some module resolutions (other than 'node') choke on this import type { PublicationEventCallbacks } from 'livekit-client/dist/src/room/track/TrackPublication'; @@ -93,11 +93,15 @@ function getTrackReferences( /** * Create `TrackReferences` for all tracks that are included in the sources property. * */ -function getParticipantTrackRefs( - participant: Participant, - identifier: ParticipantTrackIdentifier, +function getParticipantTrackRefs< + TrackSource extends ExtractTrackSourcesFromParticipantTrackIdentifier, + P extends Participant, + TrackIdentifier extends ParticipantTrackIdentifier = ParticipantTrackIdentifier, +>( + participant: P, + identifier: TrackIdentifier, onlySubscribedTracks = false, -): TrackReference[] { +): Array> { const { sources, kind, name } = identifier; const sourceReferences = Array.from(participant.trackPublications.values()) .filter( @@ -108,11 +112,11 @@ function getParticipantTrackRefs( // either return all or only the ones that are subscribed (!onlySubscribedTracks || pub.track), ) - .map((track): TrackReference => { + .map((track): TrackReference => { return { participant: participant, publication: track, - source: track.source, + source: track.source as TrackSource, }; }); @@ -157,17 +161,21 @@ export function trackReferencesObservable( return observable; } -export function participantTracksObservable( - participant: Participant, - trackIdentifier: ParticipantTrackIdentifier, -): Observable { +export function participantTracksObservable< + TrackSource extends ExtractTrackSourcesFromParticipantTrackIdentifier, + P extends Participant = Participant, + TrackIdentifier extends ParticipantTrackIdentifier = ParticipantTrackIdentifier, +>( + participant: P, + trackIdentifier: TrackIdentifier, +): Observable>> { const observable = observeParticipantEvents(participant, ...participantTrackEvents).pipe( map((participant) => { - const data = getParticipantTrackRefs(participant, trackIdentifier); + const data = getParticipantTrackRefs(participant, trackIdentifier); log.debug(`TrackReference[] was updated. (length ${data.length})`, data); return data; }), - startWith(getParticipantTrackRefs(participant, trackIdentifier)), + startWith(getParticipantTrackRefs(participant, trackIdentifier)), ); return observable; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index cdbf36aa4..2b2f2d6bd 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -57,6 +57,10 @@ export type ParticipantTrackIdentifier = RequireAtLeastOne< 'sources' | 'name' | 'kind' >; +export type ExtractTrackSourcesFromParticipantTrackIdentifier = ( + TrackIdentifier['sources'] extends undefined ? Track.Source : NonNullable[0] +); + /** * @beta */ From b773562e446ef15a6e3e472ac2905ac77e243a95 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Tue, 9 Sep 2025 13:28:50 -0400 Subject: [PATCH 004/113] feat: add manual room parameters to a few hooks / components that previously didn't accept it --- packages/react/src/components/RoomAudioRenderer.tsx | 8 ++++++-- .../react/src/components/controls/StartAudio.tsx | 9 ++++++--- packages/react/src/hooks/useSpeakingParticipants.ts | 13 ++++++++----- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/react/src/components/RoomAudioRenderer.tsx b/packages/react/src/components/RoomAudioRenderer.tsx index 3e8ea26fe..737bae98a 100644 --- a/packages/react/src/components/RoomAudioRenderer.tsx +++ b/packages/react/src/components/RoomAudioRenderer.tsx @@ -1,13 +1,16 @@ import { getTrackReferenceId } from '@livekit/components-core'; -import { Track } from 'livekit-client'; +import { Room, Track } from 'livekit-client'; import * as React from 'react'; import { useTracks } from '../hooks'; import { AudioTrack } from './participant/AudioTrack'; /** @public */ export interface RoomAudioRendererProps { + room?: Room; + /** Sets the volume for all audio tracks rendered by this component. By default, the range is between `0.0` and `1.0`. */ volume?: number; + /** * If set to `true`, mutes all audio tracks rendered by the component. * @remarks @@ -29,12 +32,13 @@ export interface RoomAudioRendererProps { * ``` * @public */ -export function RoomAudioRenderer({ volume, muted }: RoomAudioRendererProps) { +export function RoomAudioRenderer({ room, volume, muted }: RoomAudioRendererProps) { const tracks = useTracks( [Track.Source.Microphone, Track.Source.ScreenShareAudio, Track.Source.Unknown], { updateOnlyOn: [], onlySubscribed: true, + room, }, ).filter((ref) => !ref.participant.isLocal && ref.publication.kind === Track.Kind.Audio); diff --git a/packages/react/src/components/controls/StartAudio.tsx b/packages/react/src/components/controls/StartAudio.tsx index 3a251ca5c..8784de9a4 100644 --- a/packages/react/src/components/controls/StartAudio.tsx +++ b/packages/react/src/components/controls/StartAudio.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; -import { useRoomContext } from '../../context'; +import { useMaybeRoomContext } from '../../context'; import { useStartAudio } from '../../hooks'; +import { Room } from 'livekit-client'; /** @public */ export interface AllowAudioPlaybackProps extends React.ButtonHTMLAttributes { + room?: Room; label: string; } @@ -26,8 +28,9 @@ export const StartAudio: ( props: AllowAudioPlaybackProps & React.RefAttributes, ) => React.ReactNode = /* @__PURE__ */ React.forwardRef( function StartAudio({ label = 'Allow Audio', ...props }: AllowAudioPlaybackProps, ref) { - const room = useRoomContext(); - const { mergedProps } = useStartAudio({ room, props }); + const room = useMaybeRoomContext(); + const roomFallback = React.useMemo(() => props.room ?? room ?? new Room(), []); + const { mergedProps } = useStartAudio({ room: roomFallback, props }); return (