diff --git a/.changeset/eight-nails-rush.md b/.changeset/eight-nails-rush.md new file mode 100644 index 000000000..1101c6a2f --- /dev/null +++ b/.changeset/eight-nails-rush.md @@ -0,0 +1,6 @@ +--- +'@livekit/components-react': minor +'@livekit/components-core': patch +--- + +fix handling of multiple tracks of the same source from the same participant diff --git a/.changeset/quiet-yaks-argue.md b/.changeset/quiet-yaks-argue.md new file mode 100644 index 000000000..ad04c0d95 --- /dev/null +++ b/.changeset/quiet-yaks-argue.md @@ -0,0 +1,6 @@ +--- +"@livekit/components-core": patch +"@livekit/components-react": patch +--- + +fix handling of multiple tracks of the same source from the same participant diff --git a/.changeset/strong-plums-nail.md b/.changeset/strong-plums-nail.md new file mode 100644 index 000000000..97503bf10 --- /dev/null +++ b/.changeset/strong-plums-nail.md @@ -0,0 +1,6 @@ +--- +'@livekit/components-react': minor +'@livekit/component-example-next': patch +--- + +refactor `ParticipantTile` and `useParticipantTile` to trackRef and rename `TrackContext` to `TrackRefContext`. diff --git a/.changeset/warm-geese-serve.md b/.changeset/warm-geese-serve.md new file mode 100644 index 000000000..1acf2889a --- /dev/null +++ b/.changeset/warm-geese-serve.md @@ -0,0 +1,5 @@ +--- +'@livekit/components-react': minor +--- + +Update AudioTrack and VideoTrack components to accept track references. diff --git a/examples/nextjs/pages/clubhouse.tsx b/examples/nextjs/pages/clubhouse.tsx index f20dd07e9..2f2440402 100644 --- a/examples/nextjs/pages/clubhouse.tsx +++ b/examples/nextjs/pages/clubhouse.tsx @@ -8,7 +8,7 @@ import { useIsMuted, useIsSpeaking, useToken, - useTrackContext, + useTrackRefContext, useTracks, } from '@livekit/components-react'; import styles from '../styles/Clubhouse.module.scss'; @@ -86,7 +86,7 @@ const Stage = () => { }; const CustomParticipantTile = () => { - const { participant, source } = useTrackContext(); + const { participant, source } = useTrackRefContext(); const isSpeaking = useIsSpeaking(participant); const isMuted = useIsMuted(source); diff --git a/examples/nextjs/pages/customize.tsx b/examples/nextjs/pages/customize.tsx index 0b0cd0fca..ef639040d 100644 --- a/examples/nextjs/pages/customize.tsx +++ b/examples/nextjs/pages/customize.tsx @@ -9,7 +9,7 @@ import { ControlBar, GridLayout, useTracks, - TrackContext, + TrackRefContext, } from '@livekit/components-react'; import { ConnectionQuality, Room, Track } from 'livekit-client'; import styles from '../styles/Simple.module.css'; @@ -78,7 +78,7 @@ export function Stage() { <>
- + {(track) => track && (
@@ -100,7 +100,7 @@ export function Stage() {
) } -
+
diff --git a/examples/nextjs/pages/simple.tsx b/examples/nextjs/pages/simple.tsx index 4efb4ef57..9f62375bb 100644 --- a/examples/nextjs/pages/simple.tsx +++ b/examples/nextjs/pages/simple.tsx @@ -5,7 +5,7 @@ import { LiveKitRoom, ParticipantTile, RoomName, - TrackContext, + TrackRefContext, useToken, useTracks, } from '@livekit/components-react'; @@ -72,7 +72,9 @@ function Stage() { <> {screenShareTrack && } - {(track) => } + + {(track) => } + ); diff --git a/packages/core/etc/components-core.api.md b/packages/core/etc/components-core.api.md index 68e71c023..b8f567cd7 100644 --- a/packages/core/etc/components-core.api.md +++ b/packages/core/etc/components-core.api.md @@ -184,9 +184,17 @@ export function isLocal(p: Participant): boolean; // @public export function isMobileBrowser(): boolean; -// @public +// @public @deprecated export function isParticipantSourcePinned(participant: Participant, source: Track.Source, pinState: PinState | undefined): boolean; +// @public +export function isParticipantTrackReferencePinned(trackRef: TrackReference, pinState: PinState | undefined): boolean; + +// Warning: (ae-internal-missing-underscore) The name "isPlaceholderReplacement" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export function isPlaceholderReplacement(currentTrackRef: TrackReferenceOrPlaceholder, nextTrackRef: TrackReferenceOrPlaceholder): boolean; + // @public (undocumented) export function isRemote(p: Participant): boolean; @@ -501,6 +509,9 @@ export type TrackReference = { // @public (undocumented) export type TrackReferenceFilter = Parameters['0']; +// @public (undocumented) +export type TrackReferenceId = ReturnType; + // @public (undocumented) export type TrackReferenceOrPlaceholder = TrackReference | TrackReferencePlaceholder; diff --git a/packages/core/src/sorting/tile-array-update.test.ts b/packages/core/src/sorting/tile-array-update.test.ts index 8f92d2774..db40c0602 100644 --- a/packages/core/src/sorting/tile-array-update.test.ts +++ b/packages/core/src/sorting/tile-array-update.test.ts @@ -338,6 +338,7 @@ describe('Test updating the list based while considering pages.', () => { expect(flatTrackReferenceArray(result)).toStrictEqual(flatTrackReferenceArray(expected)); }); + // FIXME: mute for implementation unmute before production. test.each([ { state: [mockTrackReferencePlaceholder('A', Track.Source.Camera)], diff --git a/packages/core/src/sorting/tile-array-update.ts b/packages/core/src/sorting/tile-array-update.ts index 66cb63d91..adf0acdb3 100644 --- a/packages/core/src/sorting/tile-array-update.ts +++ b/packages/core/src/sorting/tile-array-update.ts @@ -1,7 +1,12 @@ import { differenceBy, chunk, zip } from '../helper/array-helper'; import { log } from '../logger'; import type { TrackReferenceOrPlaceholder } from '../track-reference'; -import { getTrackReferenceId } from '../track-reference'; +import { + getTrackReferenceId, + isPlaceholderReplacement, + isTrackReference, + isTrackReferencePlaceholder, +} from '../track-reference'; import { flatTrackReferenceArray } from '../track-reference/test-utils'; type VisualChanges = { @@ -80,12 +85,12 @@ export function updatePages( ): T[] { let updatedList: T[] = refreshList(currentList, nextList); - if (currentList.length < nextList.length) { + if (updatedList.length < nextList.length) { // Items got added: Find newly added items and add them to the end of the list. - const addedItems = differenceBy(nextList, currentList, getTrackReferenceId); + const addedItems = differenceBy(nextList, updatedList, getTrackReferenceId); updatedList = [...updatedList, ...addedItems]; } - const currentPages = divideIntoPages(currentList, maxItemsOnPage); + const currentPages = divideIntoPages(updatedList, maxItemsOnPage); const nextPages = divideIntoPages(nextList, maxItemsOnPage); zip(currentPages, nextPages).forEach(([currentPage, nextPage], pageIndex) => { @@ -131,7 +136,7 @@ export function updatePages( if (updatedList.length > nextList.length) { // Items got removed: Find items that got completely removed from the list. - const missingItems = differenceBy(currentList, nextList, getTrackReferenceId); + const missingItems = differenceBy(updatedList, nextList, getTrackReferenceId); updatedList = updatedList.filter( (item) => !missingItems.map(getTrackReferenceId).includes(getTrackReferenceId(item)), ); @@ -141,19 +146,24 @@ export function updatePages( } /** - * Update the first list with the items from the second list whenever the ids are the same. + * Update the current list with the items from the next list whenever the item ids are the same + * or the current item is a placeholder and we find a track reference in the next list + * to replace the placeholder with. * @remarks * This is needed because `TrackReference`s can change their internal state while keeping the same id. */ function refreshList(currentList: T[], nextList: T[]): T[] { return currentList.map((currentItem) => { const updateForCurrentItem = nextList.find( - (newItem_) => getTrackReferenceId(currentItem) === getTrackReferenceId(newItem_), + (newItem_) => + // If the IDs match or .. + getTrackReferenceId(currentItem) === getTrackReferenceId(newItem_) || + // ... if the current item is a placeholder and the new item is the track reference can replace it. + (typeof currentItem !== 'number' && + isTrackReferencePlaceholder(currentItem) && + isTrackReference(newItem_) && + isPlaceholderReplacement(currentItem, newItem_)), ); - if (updateForCurrentItem) { - return updateForCurrentItem; - } else { - return currentItem; - } + return updateForCurrentItem ?? currentItem; }); } diff --git a/packages/core/src/track-reference/test-utils.test.ts b/packages/core/src/track-reference/test-utils.test.ts index ec140ebf1..1c966f5ce 100644 --- a/packages/core/src/track-reference/test-utils.test.ts +++ b/packages/core/src/track-reference/test-utils.test.ts @@ -1,7 +1,8 @@ import { describe, test, expect, expectTypeOf } from 'vitest'; -import { mockTrackReferenceSubscribed } from './test-utils'; -import type { Participant, TrackPublication } from 'livekit-client'; +import { mockTrackReferencePlaceholder, mockTrackReferenceSubscribed } from './test-utils'; +import { Participant, TrackPublication } from 'livekit-client'; import { Track } from 'livekit-client'; +import { getTrackReferenceId } from './track-reference.utils'; describe('Test mocking functions ', () => { test('mockTrackReferenceSubscribed without options.', () => { @@ -23,3 +24,36 @@ describe('Test mocking functions ', () => { expectTypeOf(mock.source).toMatchTypeOf(); }); }); + +describe('Test mockTrackReferencePlaceholder() produces valid id with getTrackReferenceId()', () => { + test.each([ + { + participantId: 'participantA', + trackSource: Track.Source.Camera, + expected: 'participantA_camera_placeholder', + }, + ])('mockTrackReferencePlaceholder id', ({ participantId, trackSource, expected }) => { + const mock = mockTrackReferencePlaceholder(participantId, trackSource); + const trackRefId = getTrackReferenceId(mock); + expect(trackRefId.startsWith(participantId)); + expect(trackRefId.endsWith('_placeholder')); + expect(trackRefId).toBe(expected); + }); +}); + +describe('Test mockTrackReferenceSubscribed() produces valid id with getTrackReferenceId()', () => { + test.each([ + { + participantId: 'participantA', + trackSource: Track.Source.Camera, + expected: 'participantA_camera_publicationId(participantA)', + }, + ])('mockTrackReferencePlaceholder id', ({ participantId, trackSource, expected }) => { + const mock = mockTrackReferenceSubscribed(participantId, trackSource, { + mockPublication: true, + }); + const trackRefId = getTrackReferenceId(mock); + expect(trackRefId.startsWith(participantId)); + expect(trackRefId).toBe(expected); + }); +}); diff --git a/packages/core/src/track-reference/test-utils.ts b/packages/core/src/track-reference/test-utils.ts index 524578893..1cd681ef0 100644 --- a/packages/core/src/track-reference/test-utils.ts +++ b/packages/core/src/track-reference/test-utils.ts @@ -51,7 +51,7 @@ export const mockTrackReferenceSubscribed = ( ? (mockParticipant(id, options.mockIsLocal ?? false) as Participant) : new Participant(`${id}`, `${id}`), publication: options.mockPublication - ? (mockTrackPublication(id, kind, source) as TrackPublication) + ? (mockTrackPublication(`publicationId(${id})`, kind, source) as TrackPublication) : publication, source, }; diff --git a/packages/core/src/track-reference/track-reference.utils.test.ts b/packages/core/src/track-reference/track-reference.utils.test.ts new file mode 100644 index 000000000..effba1b03 --- /dev/null +++ b/packages/core/src/track-reference/track-reference.utils.test.ts @@ -0,0 +1,58 @@ +import { describe, test, expect, expectTypeOf } from 'vitest'; +import { mockTrackReferencePlaceholder, mockTrackReferenceSubscribed } from './test-utils'; +import type { Participant, TrackPublication } from 'livekit-client'; +import { Track } from 'livekit-client'; +import { isPlaceholderReplacement } from './track-reference.utils'; + +describe('Test mocking functions ', () => { + test('mockTrackReferenceSubscribed without options.', () => { + const mock = mockTrackReferenceSubscribed('MOCK_ID', Track.Source.Camera); + expect(mock).toBeDefined(); + // Check if the participant is mocked correctly: + expect(mock.participant).toBeDefined(); + expect(mock.participant.identity).toBe('MOCK_ID'); + expectTypeOf(mock.participant).toMatchTypeOf(); + + // Check if the publication is mocked correctly: + expect(mock.publication).toBeDefined(); + expect(mock.publication.kind).toBe(Track.Kind.Video); + expectTypeOf(mock.publication).toMatchTypeOf(); + + // Check if the source is mocked correctly: + expect(mock.source).toBeDefined(); + expect(mock.source).toBe(Track.Source.Camera); + expectTypeOf(mock.source).toMatchTypeOf(); + }); +}); + +describe('Test if the current TrackReferencePlaceholder can be replaced with the next TrackReference.', () => { + test.each([ + { + currentTrackRef: mockTrackReferencePlaceholder('Participant_A', Track.Source.Camera), + nextTrackRef: mockTrackReferenceSubscribed('Participant_A', Track.Source.Camera, { + mockPublication: true, + }), + isReplacement: true, + }, + { + currentTrackRef: mockTrackReferencePlaceholder('Participant_B', Track.Source.Camera), + nextTrackRef: mockTrackReferenceSubscribed('Participant_A', Track.Source.Camera, { + mockPublication: true, + }), + isReplacement: false, + }, + { + currentTrackRef: mockTrackReferencePlaceholder('Participant_A', Track.Source.ScreenShare), + nextTrackRef: mockTrackReferenceSubscribed('Participant_A', Track.Source.Camera, { + mockPublication: true, + }), + isReplacement: false, + }, + ])( + 'Test if the current TrackReference was the placeholder for the next TrackReference.', + ({ nextTrackRef: trackRef, currentTrackRef: maybePlaceholder, isReplacement }) => { + const result = isPlaceholderReplacement(maybePlaceholder, trackRef); + expect(result).toBe(isReplacement); + }, + ); +}); diff --git a/packages/core/src/track-reference/track-reference.utils.ts b/packages/core/src/track-reference/track-reference.utils.ts index 016c4a327..22f28fa05 100644 --- a/packages/core/src/track-reference/track-reference.utils.ts +++ b/packages/core/src/track-reference/track-reference.utils.ts @@ -3,17 +3,27 @@ import type { PinState } from '../types'; import type { TrackReferenceOrPlaceholder } from './track-reference.types'; import { isTrackReference, isTrackReferencePlaceholder } from './track-reference.types'; -/** Returns a id to identify the `TrackReference` based on participant and source. */ -export function getTrackReferenceId(trackReference: TrackReferenceOrPlaceholder | number): string { +/** + * Returns a id to identify the `TrackReference` or `TrackReferencePlaceholder` based on + * participant, track source and trackSid. + * @remarks + * The id pattern is: `${participantIdentity}_${trackSource}_${trackSid}` for `TrackReference` + * and `${participantIdentity}_${trackSource}_placeholder` for `TrackReferencePlaceholder`. + */ +export function getTrackReferenceId(trackReference: TrackReferenceOrPlaceholder | number) { if (typeof trackReference === 'string' || typeof trackReference === 'number') { return `${trackReference}`; + } else if (isTrackReferencePlaceholder(trackReference)) { + return `${trackReference.participant.identity}_${trackReference.source}_placeholder`; } else if (isTrackReference(trackReference)) { - return `${trackReference.participant.identity}_${trackReference.publication.source}`; + return `${trackReference.participant.identity}_${trackReference.publication.source}_${trackReference.publication.trackSid}`; } else { - return `${trackReference.participant.identity}_${trackReference.source}`; + throw new Error(`Can't generate a id for the given track reference: ${trackReference}`); } } +export type TrackReferenceId = ReturnType; + /** Returns the Source of the TrackReference. */ export function getTrackReferenceSource(trackReference: TrackReferenceOrPlaceholder): Track.Source { if (isTrackReference(trackReference)) { @@ -27,12 +37,14 @@ export function isEqualTrackRef( a?: TrackReferenceOrPlaceholder, b?: TrackReferenceOrPlaceholder, ): boolean { + if (a === undefined || b === undefined) { + return false; + } if (isTrackReference(a) && isTrackReference(b)) { return a.publication.trackSid === b.publication.trackSid; - } else if (isTrackReferencePlaceholder(a) && isTrackReferencePlaceholder(b)) { - return a.participant.identity === b.participant.identity && a.source === b.source; + } else { + return getTrackReferenceId(a) === getTrackReferenceId(b); } - return false; } /** @@ -63,3 +75,23 @@ export function isTrackReferencePinned( return false; } } + +/** + * Check if the current `currentTrackRef` is the placeholder for next `nextTrackRef`. + * Based on the participant identity and the source. + * @internal + */ +export function isPlaceholderReplacement( + currentTrackRef: TrackReferenceOrPlaceholder, + nextTrackRef: TrackReferenceOrPlaceholder, +) { + // if (typeof nextTrackRef === 'number' || typeof currentTrackRef === 'number') { + // return false; + // } + return ( + isTrackReferencePlaceholder(currentTrackRef) && + isTrackReference(nextTrackRef) && + nextTrackRef.participant.identity === currentTrackRef.participant.identity && + nextTrackRef.source === currentTrackRef.source + ); +} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 6584b96c2..0c516c5c4 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -2,6 +2,8 @@ import type { Participant, Track, TrackPublication } from 'livekit-client'; import { LocalParticipant, RemoteParticipant } from 'livekit-client'; import type { PinState } from './types'; +import type { TrackReference } from './track-reference'; +import { isEqualTrackRef } from './track-reference'; export function isLocal(p: Participant) { return p instanceof LocalParticipant; @@ -28,6 +30,7 @@ export const attachIfSubscribed = ( /** * Check if the participant track source is pinned. + * @deprecated Use {@link isParticipantTrackReferencePinned} instead. */ export function isParticipantSourcePinned( participant: Participant, @@ -44,6 +47,20 @@ export function isParticipantSourcePinned( ); } +/** + * Check if the participant track reference is pinned. + */ +export function isParticipantTrackReferencePinned( + trackRef: TrackReference, + pinState: PinState | undefined, +): boolean { + if (pinState === undefined) { + return false; + } + + return pinState.some((pinnedTrackRef) => isEqualTrackRef(pinnedTrackRef, trackRef)); +} + /** * Calculates the scrollbar width by creating two HTML elements * and messaging the difference. diff --git a/packages/react/etc/components-react.api.md b/packages/react/etc/components-react.api.md index ce546bed2..aa8e8e3b7 100644 --- a/packages/react/etc/components-react.api.md +++ b/packages/react/etc/components-react.api.md @@ -62,20 +62,21 @@ export interface AudioConferenceProps extends React_2.HTMLAttributes extends React_2.HTMLAttributes { - // (undocumented) + // @deprecated (undocumented) name?: string; // (undocumented) onSubscriptionStatusChanged?: (subscribed: boolean) => void; - // (undocumented) + // @deprecated (undocumented) participant?: Participant; - // (undocumented) + // @deprecated (undocumented) publication?: TrackPublication; - // (undocumented) - source: Track.Source; + // @deprecated (undocumented) + source?: Track.Source; + trackRef?: TrackReference; volume?: number; } @@ -200,16 +201,16 @@ export interface DisconnectButtonProps extends React_2.ButtonHTMLAttributes { - // (undocumented) + // @deprecated (undocumented) focusTrack?: TrackReference; - // (undocumented) + // @deprecated (undocumented) participants?: Array; } @@ -217,19 +218,22 @@ export interface FocusLayoutContainerProps extends React_2.HTMLAttributes { // (undocumented) onParticipantClick?: (evt: ParticipantClickEvent) => void; - // (undocumented) + // @deprecated (undocumented) track?: TrackReferenceOrPlaceholder; + trackRef?: TrackReferenceOrPlaceholder; } // @public -export function FocusToggle({ trackSource, participant, ...props }: FocusToggleProps): React_2.JSX.Element; +export function FocusToggle({ trackRef, trackSource, participant, ...props }: FocusToggleProps): React_2.JSX.Element; // @public (undocumented) export interface FocusToggleProps extends React_2.ButtonHTMLAttributes { - // (undocumented) + // @deprecated (undocumented) participant?: Participant; // (undocumented) - trackSource: Track.Source; + trackRef?: TrackReferenceOrPlaceholder; + // @deprecated (undocumented) + trackSource?: Track.Source; } // @public (undocumented) @@ -379,7 +383,7 @@ export interface ParticipantNameProps extends React_2.HTMLAttributes { @@ -387,12 +391,13 @@ export interface ParticipantTileProps extends React_2.HTMLAttributes void; - // (undocumented) + // @deprecated (undocumented) participant?: Participant; - // (undocumented) + // @deprecated (undocumented) publication?: TrackPublication; - // (undocumented) + // @deprecated (undocumented) source?: Track.Source; + trackRef?: TrackReferenceOrPlaceholder; } // @public @@ -441,7 +446,7 @@ export function StartAudio({ label, ...props }: AllowAudioPlaybackProps): React_ // @public (undocumented) export function Toast(props: React_2.HTMLAttributes): React_2.JSX.Element; -// @public (undocumented) +// @public @deprecated (undocumented) export const TrackContext: React_2.Context; // @public @@ -466,6 +471,9 @@ export interface TrackMutedIndicatorProps extends React_2.HTMLAttributes; + // @public export function TrackToggle({ showIcon, ...props }: TrackToggleProps): React_2.JSX.Element; @@ -566,13 +574,16 @@ export function useEnsureParticipant(participant?: Participant): Participant; export function useEnsureRoom(room?: Room): Room; // @public +export function useEnsureTrackRef(trackRef?: TrackReferenceOrPlaceholder): void; + +// @public @deprecated export function useEnsureTrackReference(track?: TrackReferenceOrPlaceholder): TrackReferenceOrPlaceholder; // @alpha export function useFacingMode(trackReference: TrackReferenceOrPlaceholder): 'user' | 'environment' | 'left' | 'right' | 'undefined'; // @public (undocumented) -export function useFocusToggle({ trackSource, participant, props }: UseFocusToggleProps): { +export function useFocusToggle({ trackRef, trackSource, participant, props }: UseFocusToggleProps): { mergedProps: React_2.ButtonHTMLAttributes & { className: string; onClick: (event: React_2.MouseEvent) => void; @@ -582,12 +593,14 @@ export function useFocusToggle({ trackSource, participant, props }: UseFocusTogg // @public (undocumented) export interface UseFocusToggleProps { - // (undocumented) + // @deprecated (undocumented) participant?: Participant; // (undocumented) props: React_2.ButtonHTMLAttributes; // (undocumented) - trackSource: Track.Source; + trackRef?: TrackReferenceOrPlaceholder; + // @deprecated (undocumented) + trackSource?: Track.Source; } // @public @@ -647,9 +660,12 @@ export function useMaybeParticipantContext(): Participant | undefined; // @public export function useMaybeRoomContext(): Room | undefined; -// @public +// @public @deprecated export function useMaybeTrackContext(): TrackReferenceOrPlaceholder | undefined; +// @public +export function useMaybeTrackRefContext(): TrackReferenceOrPlaceholder | undefined; + // @public (undocumented) export function useMediaDevices({ kind }: { kind: MediaDeviceKind; @@ -747,7 +763,7 @@ export interface UseParticipantsOptions { } // @public (undocumented) -export function useParticipantTile({ participant, source, publication, onParticipantClick, disableSpeakingIndicator, htmlProps, }: UseParticipantTileProps): { +export function useParticipantTile({ trackRef, participant, source, publication, onParticipantClick, disableSpeakingIndicator, htmlProps, }: UseParticipantTileProps): { elementProps: React_2.HTMLAttributes; }; @@ -759,12 +775,13 @@ export interface UseParticipantTileProps extends React_2. htmlProps: React_2.HTMLAttributes; // (undocumented) onParticipantClick?: (event: ParticipantClickEvent) => void; - // (undocumented) + // @deprecated (undocumented) participant: Participant; - // (undocumented) + // @deprecated (undocumented) publication?: TrackPublication; - // (undocumented) + // @deprecated (undocumented) source: Track.Source; + trackRef?: TrackReferenceOrPlaceholder; } // @public (undocumented) @@ -868,7 +885,7 @@ export interface UseTokenOptions { userInfo?: UserInfo; } -// @public +// @public @deprecated export function useTrackContext(): TrackReferenceOrPlaceholder; // @public (undocumented) @@ -883,6 +900,9 @@ export interface UseTrackMutedIndicatorOptions { participant?: Participant; } +// @public +export function useTrackRefContext(): TrackReferenceOrPlaceholder; + // @public export function useTracks(sources?: T, options?: UseTracksOptions): UseTracksHookReturnType; @@ -932,24 +952,25 @@ export interface VideoConferenceProps extends React_2.HTMLAttributes { // (undocumented) manageSubscription?: boolean; - // (undocumented) + // @deprecated (undocumented) name?: string; // (undocumented) onSubscriptionStatusChanged?: (subscribed: boolean) => void; // (undocumented) onTrackClick?: (evt: ParticipantClickEvent) => void; - // (undocumented) + // @deprecated (undocumented) participant?: Participant; - // (undocumented) + // @deprecated (undocumented) publication?: TrackPublication; - // (undocumented) - source: Track.Source; + // @deprecated (undocumented) + source?: Track.Source; + trackRef?: TrackReference; } // Warnings were encountered during analysis: diff --git a/packages/react/src/components/RoomAudioRenderer.tsx b/packages/react/src/components/RoomAudioRenderer.tsx index 4e8689abe..d59a21619 100644 --- a/packages/react/src/components/RoomAudioRenderer.tsx +++ b/packages/react/src/components/RoomAudioRenderer.tsx @@ -1,4 +1,4 @@ -import { isLocal } from '@livekit/components-core'; +import { getTrackReferenceId, isLocal } from '@livekit/components-core'; import type { RemoteTrackPublication } from 'livekit-client'; import { Track } from 'livekit-client'; import * as React from 'react'; @@ -30,7 +30,7 @@ export function RoomAudioRenderer() { return (
{tracks.map((trackRef) => ( - + ))}
); diff --git a/packages/react/src/components/TrackLoop.tsx b/packages/react/src/components/TrackLoop.tsx index 691b6470c..8d2173d18 100644 --- a/packages/react/src/components/TrackLoop.tsx +++ b/packages/react/src/components/TrackLoop.tsx @@ -1,8 +1,8 @@ import type { TrackReference, TrackReferenceOrPlaceholder } from '@livekit/components-core'; -import { isTrackReference } from '@livekit/components-core'; import * as React from 'react'; -import { TrackContext } from '../context/track-context'; +import { TrackRefContext } from '../context/track-reference-context'; import { cloneSingleChild } from '../utils'; +import { getTrackReferenceId } from '@livekit/components-core'; /** @public */ export interface TrackLoopProps { @@ -14,15 +14,15 @@ export interface TrackLoopProps { /** * The TrackLoop component loops over tracks. It is for example a easy way to loop over all participant camera and screen share tracks. - * TrackLoop creates a TrackContext for each track that you can use to e.g. render the track. + * TrackLoop creates a TrackRefContext for each track that you can use to e.g. render the track. * * @example * ```tsx - * const tracks = useTracks([Track.Source.Camera]); - * - * - * {(track) => track && } - * + * const trackRefs = useTracks([Track.Source.Camera]); + * + * + * {(trackRef) => trackRef && } + * * * ``` * @public @@ -31,16 +31,13 @@ export function TrackLoop({ tracks, ...props }: TrackLoopProps) { return ( <> {tracks.map((trackReference) => { - const trackSource = isTrackReference(trackReference) - ? trackReference.publication.source - : trackReference.source; return ( - {cloneSingleChild(props.children)} - + ); })} diff --git a/packages/react/src/components/controls/FocusToggle.tsx b/packages/react/src/components/controls/FocusToggle.tsx index 848ba6418..0fc71c5c5 100644 --- a/packages/react/src/components/controls/FocusToggle.tsx +++ b/packages/react/src/components/controls/FocusToggle.tsx @@ -1,12 +1,16 @@ import type { Participant, Track } from 'livekit-client'; import * as React from 'react'; -import { LayoutContext } from '../../context'; +import { LayoutContext, useMaybeTrackRefContext } from '../../context'; import { FocusToggleIcon, UnfocusToggleIcon } from '../../assets/icons'; import { useFocusToggle } from '../../hooks'; +import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'; /** @public */ export interface FocusToggleProps extends React.ButtonHTMLAttributes { - trackSource: Track.Source; + trackRef?: TrackReferenceOrPlaceholder; + /** @deprecated This parameter will be removed in a future version use `trackRef` instead. */ + trackSource?: Track.Source; + /** @deprecated This parameter will be removed in a future version use `trackRef` instead. */ participant?: Participant; } @@ -21,8 +25,15 @@ export interface FocusToggleProps extends React.ButtonHTMLAttributes diff --git a/packages/react/src/components/layout/FocusLayout.tsx b/packages/react/src/components/layout/FocusLayout.tsx index 9e5c0e32d..ec73dddf0 100644 --- a/packages/react/src/components/layout/FocusLayout.tsx +++ b/packages/react/src/components/layout/FocusLayout.tsx @@ -7,7 +7,9 @@ import type { ParticipantClickEvent } from '@livekit/components-core'; /** @public */ export interface FocusLayoutContainerProps extends React.HTMLAttributes { + /** @deprecated This property has no effect and will be removed in a future version. */ focusTrack?: TrackReference; + /** @deprecated This property has no effect and will be removed in a future version. */ participants?: Array; } @@ -20,11 +22,15 @@ export function FocusLayoutContainer(props: FocusLayoutContainerProps) { /** @public */ export interface FocusLayoutProps extends React.HTMLAttributes { + /** The track to display in the focus layout. */ + trackRef?: TrackReferenceOrPlaceholder; + /** @deprecated This property will be removed in a future version use `trackRef` instead. */ track?: TrackReferenceOrPlaceholder; onParticipantClick?: (evt: ParticipantClickEvent) => void; } /** @public */ -export function FocusLayout({ track, ...htmlProps }: FocusLayoutProps) { - return ; +export function FocusLayout({ trackRef, track, ...htmlProps }: FocusLayoutProps) { + const trackReference = trackRef ?? track; + return ; } diff --git a/packages/react/src/components/participant/AudioTrack.tsx b/packages/react/src/components/participant/AudioTrack.tsx index a04f1410b..c6852f268 100644 --- a/packages/react/src/components/participant/AudioTrack.tsx +++ b/packages/react/src/components/participant/AudioTrack.tsx @@ -1,16 +1,23 @@ import type { Participant, Track, TrackPublication } from 'livekit-client'; import * as React from 'react'; import { useMediaTrackBySourceOrName } from '../../hooks/useMediaTrackBySourceOrName'; +import type { TrackReference } from '@livekit/components-core'; import { log } from '@livekit/components-core'; -import { useEnsureParticipant } from '../../context'; +import { useEnsureParticipant, useMaybeTrackRefContext } from '../../context'; import { RemoteAudioTrack } from 'livekit-client'; /** @public */ export interface AudioTrackProps extends React.HTMLAttributes { - source: Track.Source; + /** The track reference of the track from which the audio is to be rendered. */ + trackRef?: TrackReference; + /** @deprecated This property will be removed in a future version use `trackRef` instead. */ + source?: Track.Source; + /** @deprecated This property will be removed in a future version use `trackRef` instead. */ name?: string; + /** @deprecated This property will be removed in a future version use `trackRef` instead. */ participant?: Participant; + /** @deprecated This property will be removed in a future version use `trackRef` instead. */ publication?: TrackPublication; onSubscriptionStatusChanged?: (subscribed: boolean) => void; /** by the default the range is between 0 and 1 */ @@ -24,7 +31,7 @@ export interface AudioTrackProps * @example * ```tsx * - * + * * * ``` * @@ -32,6 +39,7 @@ export interface AudioTrackProps * @public */ export function AudioTrack({ + trackRef, onSubscriptionStatusChanged, volume, source, @@ -40,11 +48,21 @@ export function AudioTrack({ participant: p, ...props }: AudioTrackProps) { + // TODO: Remove and refactor all variables with underscore in a future version after the deprecation period. + const maybeTrackRef = useMaybeTrackRefContext(); + const _name = trackRef?.publication?.trackName ?? maybeTrackRef?.publication?.trackName ?? name; + const _source = trackRef?.source ?? maybeTrackRef?.source ?? source; + const _publication = trackRef?.publication ?? maybeTrackRef?.publication ?? publication; + const _participant = trackRef?.participant ?? maybeTrackRef?.participant ?? p; + if (_source === undefined) { + throw new Error('The AudioTrack component expects a trackRef or source property.'); + } + const mediaEl = React.useRef(null); - const participant = useEnsureParticipant(p); + const participant = useEnsureParticipant(_participant); const { elementProps, isSubscribed, track } = useMediaTrackBySourceOrName( - { source, name, participant, publication }, + { source: _source, name: _name, participant, publication: _publication }, { element: mediaEl, props, diff --git a/packages/react/src/components/participant/ParticipantTile.tsx b/packages/react/src/components/participant/ParticipantTile.tsx index 390ef6768..2e6f69113 100644 --- a/packages/react/src/components/participant/ParticipantTile.tsx +++ b/packages/react/src/components/participant/ParticipantTile.tsx @@ -2,16 +2,17 @@ import * as React from 'react'; import type { Participant, TrackPublication } from 'livekit-client'; import { Track } from 'livekit-client'; import type { ParticipantClickEvent, TrackReferenceOrPlaceholder } from '@livekit/components-core'; -import { isParticipantSourcePinned } from '@livekit/components-core'; +import { isTrackReference, isTrackReferencePinned } from '@livekit/components-core'; import { ConnectionQualityIndicator } from './ConnectionQualityIndicator'; import { ParticipantName } from './ParticipantName'; import { TrackMutedIndicator } from './TrackMutedIndicator'; import { ParticipantContext, + TrackRefContext, useEnsureParticipant, useMaybeLayoutContext, useMaybeParticipantContext, - useMaybeTrackContext, + useMaybeTrackRefContext, } from '../../context'; import { FocusToggle } from '../controls/FocusToggle'; import { ParticipantPlaceholder } from '../../assets/images'; @@ -37,28 +38,54 @@ export function ParticipantContextIfNeeded( ); } +/** + * Only create a `TrackRefContext` if there is no `TrackRefContext` already. + */ +function TrackRefContextIfNeeded( + props: React.PropsWithChildren<{ + trackRef?: TrackReferenceOrPlaceholder; + }>, +) { + const hasContext = !!useMaybeTrackRefContext(); + return props.trackRef && !hasContext ? ( + {props.children} + ) : ( + <>{props.children} + ); +} + /** @public */ export interface ParticipantTileProps extends React.HTMLAttributes { + /** The track reference to display. */ + trackRef?: TrackReferenceOrPlaceholder; disableSpeakingIndicator?: boolean; + /** @deprecated This parameter will be removed in a future version use `trackRef` instead. */ participant?: Participant; + /** @deprecated This parameter will be removed in a future version use `trackRef` instead. */ source?: Track.Source; + /** @deprecated This parameter will be removed in a future version use `trackRef` instead. */ publication?: TrackPublication; onParticipantClick?: (event: ParticipantClickEvent) => void; } /** * The ParticipantTile component is the base utility wrapper for displaying a visual representation of a participant. - * This component can be used as a child of the `TrackLoop` component or by spreading a track reference as properties. + * This component can be used as a child of the `TrackLoop` component or by passing a track reference as property. * - * @example + * @example Using the `ParticipantTile` component with a track reference: * ```tsx - * - * - * + * + * ``` + * @example Using the `ParticipantTile` component as a child of the `TrackLoop` component: + * ```tsx + * + * + * * ``` * @public */ export function ParticipantTile({ + trackRef, participant, children, source = Track.Source.Camera, @@ -67,18 +94,22 @@ export function ParticipantTile({ disableSpeakingIndicator, ...htmlProps }: ParticipantTileProps) { + // TODO: remove deprecated props and refactor in a future version. + const maybeTrackRef = useMaybeTrackRefContext(); const p = useEnsureParticipant(participant); - const trackRef: TrackReferenceOrPlaceholder = useMaybeTrackContext() ?? { - participant: p, - source, - publication, - }; + const trackReference: TrackReferenceOrPlaceholder = React.useMemo(() => { + return { + participant: trackRef?.participant ?? maybeTrackRef?.participant ?? p, + source: trackRef?.source ?? maybeTrackRef?.source ?? source, + publication: trackRef?.publication ?? maybeTrackRef?.publication ?? publication, + }; + }, [maybeTrackRef, p, publication, source, trackRef]); const { elementProps } = useParticipantTile({ - participant: trackRef.participant, + participant: trackReference.participant, htmlProps, - source: trackRef.source, - publication: trackRef.publication, + source: trackReference.source, + publication: trackReference.publication, disableSpeakingIndicator, onParticipantClick, }); @@ -88,68 +119,69 @@ export function ParticipantTile({ const handleSubscribe = React.useCallback( (subscribed: boolean) => { if ( - trackRef.source && + trackReference.source && !subscribed && layoutContext && layoutContext.pin.dispatch && - isParticipantSourcePinned(trackRef.participant, trackRef.source, layoutContext.pin.state) + isTrackReferencePinned(trackReference, layoutContext.pin.state) ) { layoutContext.pin.dispatch({ msg: 'clear_pin' }); } }, - [trackRef.participant, layoutContext, trackRef.source], + [trackReference, layoutContext], ); return (
- - {children ?? ( - <> - {trackRef.publication?.kind === 'video' || - trackRef.source === Track.Source.Camera || - trackRef.source === Track.Source.ScreenShare ? ( - - ) : ( - - )} -
- -
-
-
- {trackRef.source === Track.Source.Camera ? ( - <> - {isEncrypted && } - - - - ) : ( - <> - - 's screen - - )} + + + {children ?? ( + <> + {isTrackReference(trackReference) && + (trackReference.publication?.kind === 'video' || + trackReference.source === Track.Source.Camera || + trackReference.source === Track.Source.ScreenShare) ? ( + + ) : ( + isTrackReference(trackReference) && ( + + ) + )} +
+ +
+
+
+ {trackReference.source === Track.Source.Camera ? ( + <> + {isEncrypted && } + + + + ) : ( + <> + + 's screen + + )} +
+
- -
- - )} - - + + )} + + +
); } diff --git a/packages/react/src/components/participant/VideoTrack.tsx b/packages/react/src/components/participant/VideoTrack.tsx index 20115aec8..08c473e93 100644 --- a/packages/react/src/components/participant/VideoTrack.tsx +++ b/packages/react/src/components/participant/VideoTrack.tsx @@ -6,15 +6,21 @@ import { } from 'livekit-client'; import * as React from 'react'; import { useMediaTrackBySourceOrName } from '../../hooks/useMediaTrackBySourceOrName'; -import type { ParticipantClickEvent } from '@livekit/components-core'; -import { useEnsureParticipant } from '../../context'; +import type { ParticipantClickEvent, TrackReference } from '@livekit/components-core'; +import { useEnsureParticipant, useMaybeTrackRefContext } from '../../context'; import * as useHooks from 'usehooks-ts'; /** @public */ export interface VideoTrackProps extends React.HTMLAttributes { - source: Track.Source; + /** The track reference of the track to render. */ + trackRef?: TrackReference; + /** @deprecated This property will be removed in a future version use `trackRef` instead. */ + source?: Track.Source; + /** @deprecated This property will be removed in a future version use `trackRef` instead. */ name?: string; + /** @deprecated This property will be removed in a future version use `trackRef` instead. */ participant?: Participant; + /** @deprecated This property will be removed in a future version use `trackRef` instead. */ publication?: TrackPublication; onTrackClick?: (evt: ParticipantClickEvent) => void; onSubscriptionStatusChanged?: (subscribed: boolean) => void; @@ -27,7 +33,7 @@ export interface VideoTrackProps extends React.HTMLAttributes * * @example * ```tsx - * + * * ``` * @see {@link @livekit/components-react#ParticipantTile | ParticipantTile} * @public @@ -36,6 +42,7 @@ export function VideoTrack({ onTrackClick, onClick, onSubscriptionStatusChanged, + trackRef, name, publication, source, @@ -43,6 +50,18 @@ export function VideoTrack({ manageSubscription, ...props }: VideoTrackProps) { + // TODO: Remove and refactor all variables with underscore in a future version after the deprecation period. + const maybeTrackRef = useMaybeTrackRefContext(); + const _name = trackRef?.publication?.trackName ?? maybeTrackRef?.publication?.trackName ?? name; + const _source = trackRef?.source ?? maybeTrackRef?.source ?? source; + const _publication = trackRef?.publication ?? maybeTrackRef?.publication ?? publication; + const _participant = trackRef?.participant ?? maybeTrackRef?.participant ?? p; + if (_source === undefined) { + throw new Error('VideoTrack: You must provide a trackRef or source property.'); + } + + const participant = useEnsureParticipant(_participant); + const mediaEl = React.useRef(null); const intersectionEntry = useHooks.useIntersectionObserver(mediaEl, {}); @@ -52,31 +71,30 @@ export function VideoTrack({ React.useEffect(() => { if ( manageSubscription && - publication instanceof RemoteTrackPublication && + _publication instanceof RemoteTrackPublication && debouncedIntersectionEntry?.isIntersecting === false && intersectionEntry?.isIntersecting === false ) { - publication.setSubscribed(false); + _publication.setSubscribed(false); } - }, [debouncedIntersectionEntry, publication, manageSubscription]); + }, [debouncedIntersectionEntry, _publication, manageSubscription]); React.useEffect(() => { if ( manageSubscription && - publication instanceof RemoteTrackPublication && + _publication instanceof RemoteTrackPublication && intersectionEntry?.isIntersecting === true ) { - publication.setSubscribed(true); + _publication.setSubscribed(true); } - }, [intersectionEntry, publication, manageSubscription]); + }, [intersectionEntry, _publication, manageSubscription]); - const participant = useEnsureParticipant(p); const { elementProps, publication: pub, isSubscribed, } = useMediaTrackBySourceOrName( - { participant, name, source, publication }, + { participant, name: _name, source: _source, publication: _publication }, { element: mediaEl, props, diff --git a/packages/react/src/context/index.ts b/packages/react/src/context/index.ts index eaebdaa52..cdd75b153 100644 --- a/packages/react/src/context/index.ts +++ b/packages/react/src/context/index.ts @@ -18,7 +18,11 @@ export {} from './pin-context'; export { RoomContext, useEnsureRoom, useMaybeRoomContext, useRoomContext } from './room-context'; export { TrackContext, + TrackRefContext, useEnsureTrackReference, + useEnsureTrackRef, useMaybeTrackContext, + useMaybeTrackRefContext, useTrackContext, -} from './track-context'; + useTrackRefContext, +} from './track-reference-context'; diff --git a/packages/react/src/context/participant-context.ts b/packages/react/src/context/participant-context.ts index 03ace1bc0..425f5653f 100644 --- a/packages/react/src/context/participant-context.ts +++ b/packages/react/src/context/participant-context.ts @@ -1,6 +1,6 @@ import type { Participant } from 'livekit-client'; import * as React from 'react'; -import { useMaybeTrackContext } from './track-context'; +import { useMaybeTrackRefContext } from './track-reference-context'; /** @public */ export const ParticipantContext = React.createContext(undefined); @@ -33,7 +33,7 @@ export function useMaybeParticipantContext() { */ export function useEnsureParticipant(participant?: Participant) { const context = useMaybeParticipantContext(); - const trackContext = useMaybeTrackContext(); + const trackContext = useMaybeTrackRefContext(); const p = participant ?? context ?? trackContext?.participant; if (!p) { throw new Error( diff --git a/packages/react/src/context/track-context.ts b/packages/react/src/context/track-context.ts deleted file mode 100644 index 1a73f7c0b..000000000 --- a/packages/react/src/context/track-context.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'; -import * as React from 'react'; - -/** @public */ -export const TrackContext = React.createContext(undefined); - -/** - * Ensures that a track reference is provided via context. - * If not inside a `TrackContext`, an error is thrown. - * @public - */ -export function useTrackContext() { - const trackReference = React.useContext(TrackContext); - if (!trackReference) { - throw Error('tried to access track context outside of track context provider'); - } - return trackReference; -} - -/** - * Returns a track reference from the `TrackContext` if it exists, otherwise `undefined`. - * @public - */ -export function useMaybeTrackContext() { - return React.useContext(TrackContext); -} - -/** - * Ensures that a track reference is provided, either via context or explicitly as a parameter. - * If not inside a `TrackContext` and no track reference is provided, an error is thrown. - * @public - */ -export function useEnsureTrackReference(track?: TrackReferenceOrPlaceholder) { - const context = useMaybeTrackContext(); - const trackRef = track ?? context; - if (!trackRef) { - throw new Error( - 'No TrackReference provided, make sure you are inside a track context or pass the track reference explicitly', - ); - } - return trackRef; -} diff --git a/packages/react/src/context/track-reference-context.ts b/packages/react/src/context/track-reference-context.ts new file mode 100644 index 000000000..d52ed7786 --- /dev/null +++ b/packages/react/src/context/track-reference-context.ts @@ -0,0 +1,80 @@ +import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'; +import * as React from 'react'; + +/** + * @public + * @deprecated `TrackContext` has been to `TrackRefContext`, use this as a drop in replacement. + */ +export const TrackContext = React.createContext(undefined); + +/** + * This context provides a `TrackReferenceOrPlaceholder` to all child components. + * @public + */ +export const TrackRefContext = TrackContext; + +/** + * Ensures that a track reference is provided via context. + * If not inside a `TrackRefContext`, an error is thrown. + * @public + * @deprecated `useTrackContext` has been to `useTrackRefContext`, use this as a drop in replacement. + */ +export function useTrackContext() { + const trackReference = React.useContext(TrackContext); + if (!trackReference) { + throw Error('tried to access track context outside of track context provider'); + } + return trackReference; +} + +/** + * Ensures that a track reference is provided via context. + * If not inside a `TrackRefContext`, an error is thrown. + * @public + */ +export function useTrackRefContext() { + return useTrackContext(); +} + +/** + * Returns a track reference from the `TrackContext` if it exists, otherwise `undefined`. + * @public + * @deprecated `useMaybeTrackContext` has been to `useMaybeTrackRefContext`, use this as a drop in replacement. + */ +export function useMaybeTrackContext() { + return React.useContext(TrackContext); +} + +/** + * Returns a track reference from the `TrackRefContext` if it exists, otherwise `undefined`. + * @public + */ +export function useMaybeTrackRefContext() { + return useMaybeTrackContext(); +} + +/** + * Ensures that a track reference is provided, either via context or explicitly as a parameter. + * If not inside a `TrackContext` and no track reference is provided, an error is thrown. + * @public + * @deprecated `useEnsureTrackReference` has been to `useEnsureTrackRef`, use this as a drop in replacement. + */ +export function useEnsureTrackReference(track?: TrackReferenceOrPlaceholder) { + const context = useMaybeTrackContext(); + const trackRef = track ?? context; + if (!trackRef) { + throw new Error( + 'No TrackRef, make sure you are inside a TrackRefContext or pass the TrackRef explicitly', + ); + } + return trackRef; +} + +/** + * Ensures that a track reference is provided, either via context or explicitly as a parameter. + * If not inside a `TrackRefContext` and no track reference is provided, an error is thrown. + * @public + */ +export function useEnsureTrackRef(trackRef?: TrackReferenceOrPlaceholder) { + useEnsureTrackReference(trackRef); +} diff --git a/packages/react/src/hooks/useFocusToggle.ts b/packages/react/src/hooks/useFocusToggle.ts index 9770e4e6b..8c9e849b6 100644 --- a/packages/react/src/hooks/useFocusToggle.ts +++ b/packages/react/src/hooks/useFocusToggle.ts @@ -1,4 +1,9 @@ -import { setupFocusToggle, isTrackReferencePinned } from '@livekit/components-core'; +import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'; +import { + setupFocusToggle, + isTrackReferencePinned, + isTrackReference, +} from '@livekit/components-core'; import type { Track, Participant } from 'livekit-client'; import { useEnsureParticipant, useMaybeLayoutContext } from '../context'; import { mergeProps } from '../mergeProps'; @@ -6,28 +11,40 @@ import * as React from 'react'; /** @public */ export interface UseFocusToggleProps { - props: React.ButtonHTMLAttributes; - trackSource: Track.Source; + trackRef?: TrackReferenceOrPlaceholder; + /** @deprecated This parameter will be removed in a future version use `trackRef` instead. */ + trackSource?: Track.Source; + /** @deprecated This parameter will be removed in a future version use `trackRef` instead. */ participant?: Participant; + props: React.ButtonHTMLAttributes; } /** @public */ -export function useFocusToggle({ trackSource, participant, props }: UseFocusToggleProps) { +export function useFocusToggle({ trackRef, trackSource, participant, props }: UseFocusToggleProps) { const p = useEnsureParticipant(participant); + if (!trackRef && !trackSource) { + throw new Error('trackRef or trackSource must be defined.'); + } const layoutContext = useMaybeLayoutContext(); const { className } = React.useMemo(() => setupFocusToggle(), []); const inFocus: boolean = React.useMemo(() => { - const track = p.getTrack(trackSource); - if (layoutContext?.pin.state && track) { - return isTrackReferencePinned( - { participant: p, source: trackSource, publication: track }, - layoutContext.pin.state, - ); + if (trackRef) { + return isTrackReferencePinned(trackRef, layoutContext?.pin.state); + } else if (trackSource) { + const track = p.getTrack(trackSource); + if (layoutContext?.pin.state && track) { + return isTrackReferencePinned( + { participant: p, source: trackSource, publication: track }, + layoutContext.pin.state, + ); + } else { + return false; + } } else { - return false; + throw new Error('trackRef or trackSource and participant must be defined.'); } - }, [p, trackSource, layoutContext]); + }, [trackRef, layoutContext?.pin.state, p, trackSource]); const mergedProps = React.useMemo( () => @@ -38,26 +55,39 @@ export function useFocusToggle({ trackSource, participant, props }: UseFocusTogg props.onClick?.(event); // Set or clear focus based on current focus state. - const track = p.getTrack(trackSource); - if (layoutContext?.pin.dispatch && track) { + if (trackRef && isTrackReference(trackRef)) { if (inFocus) { - layoutContext.pin.dispatch({ + layoutContext?.pin.dispatch?.({ msg: 'clear_pin', }); } else { - layoutContext.pin.dispatch({ + layoutContext?.pin.dispatch?.({ msg: 'set_pin', - trackReference: { - participant: p, - publication: track, - source: track.source, - }, + trackReference: trackRef, }); } + } else if (trackSource) { + const track = p.getTrack(trackSource); + if (layoutContext?.pin.dispatch && track) { + if (inFocus) { + layoutContext.pin.dispatch({ + msg: 'clear_pin', + }); + } else { + layoutContext.pin.dispatch({ + msg: 'set_pin', + trackReference: { + participant: p, + publication: track, + source: track.source, + }, + }); + } + } } }, }), - [props, className, p, trackSource, inFocus, layoutContext], + [props, className, trackRef, trackSource, inFocus, layoutContext?.pin, p], ); return { mergedProps, inFocus }; diff --git a/packages/react/src/hooks/useParticipantTile.ts b/packages/react/src/hooks/useParticipantTile.ts index 2d82f0aae..80b806a64 100644 --- a/packages/react/src/hooks/useParticipantTile.ts +++ b/packages/react/src/hooks/useParticipantTile.ts @@ -1,9 +1,9 @@ -import type { ParticipantClickEvent } from '@livekit/components-core'; +import type { ParticipantClickEvent, TrackReferenceOrPlaceholder } from '@livekit/components-core'; import { setupParticipantTile } from '@livekit/components-core'; import type { TrackPublication, Participant } from 'livekit-client'; import { Track } from 'livekit-client'; import * as React from 'react'; -import { useEnsureParticipant } from '../context'; +import { useEnsureParticipant, useMaybeTrackRefContext } from '../context'; import { mergeProps } from '../mergeProps'; import { useFacingMode } from './useFacingMode'; import { useIsMuted } from './useIsMuted'; @@ -11,16 +11,22 @@ import { useIsSpeaking } from './useIsSpeaking'; /** @public */ export interface UseParticipantTileProps extends React.HTMLAttributes { + /** The track reference to display. */ + trackRef?: TrackReferenceOrPlaceholder; disableSpeakingIndicator?: boolean; + /** @deprecated This parameter will be removed in a future version use `trackRef` instead. */ publication?: TrackPublication; onParticipantClick?: (event: ParticipantClickEvent) => void; htmlProps: React.HTMLAttributes; + /** @deprecated This parameter will be removed in a future version use `trackRef` instead. */ source: Track.Source; + /** @deprecated This parameter will be removed in a future version use `trackRef` instead. */ participant: Participant; } /** @public */ export function useParticipantTile({ + trackRef, participant, source, publication, @@ -28,7 +34,28 @@ export function useParticipantTile({ disableSpeakingIndicator, htmlProps, }: UseParticipantTileProps) { + // TODO: Remove and refactor after deprecation period to use: + // const trackReference = useEnsureTrackRefContext(trackRef)`. + const maybeTrackRef = useMaybeTrackRefContext(); const p = useEnsureParticipant(participant); + const trackReference = React.useMemo(() => { + return { + participant: trackRef?.participant ?? maybeTrackRef?.participant ?? p, + source: trackRef?.source ?? maybeTrackRef?.source ?? source, + publication: trackRef?.publication ?? maybeTrackRef?.publication ?? publication, + }; + }, [ + trackRef?.participant, + trackRef?.source, + trackRef?.publication, + maybeTrackRef?.participant, + maybeTrackRef?.source, + maybeTrackRef?.publication, + p, + source, + publication, + ]); + const mergedProps = React.useMemo(() => { const { className } = setupParticipantTile(); return mergeProps(htmlProps, { @@ -36,23 +63,33 @@ export function useParticipantTile({ onClick: (event: React.MouseEvent) => { htmlProps.onClick?.(event); if (typeof onParticipantClick === 'function') { - const track = publication ?? p.getTrack(source); - onParticipantClick({ participant: p, track }); + const track = + trackReference.publication ?? + trackReference.participant.getTrack(trackReference.source); + onParticipantClick({ participant: trackReference.participant, track }); } }, }); - }, [htmlProps, source, onParticipantClick, p, publication]); - const isVideoMuted = useIsMuted(Track.Source.Camera, { participant }); - const isAudioMuted = useIsMuted(Track.Source.Microphone, { participant }); - const isSpeaking = useIsSpeaking(participant); - const facingMode = useFacingMode({ participant, publication, source }); + }, [ + htmlProps, + onParticipantClick, + trackReference.publication, + trackReference.source, + trackReference.participant, + ]); + const isVideoMuted = useIsMuted(Track.Source.Camera, { participant: trackReference.participant }); + const isAudioMuted = useIsMuted(Track.Source.Microphone, { + participant: trackReference.participant, + }); + const isSpeaking = useIsSpeaking(trackReference.participant); + const facingMode = useFacingMode(trackReference); return { elementProps: { 'data-lk-audio-muted': isAudioMuted, 'data-lk-video-muted': isVideoMuted, 'data-lk-speaking': disableSpeakingIndicator === true ? false : isSpeaking, - 'data-lk-local-participant': participant.isLocal, - 'data-lk-source': source, + 'data-lk-local-participant': trackReference.participant.isLocal, + 'data-lk-source': trackReference.source, 'data-lk-facing-mode': facingMode, ...mergedProps, } as React.HTMLAttributes, diff --git a/packages/react/src/prefabs/VideoConference.tsx b/packages/react/src/prefabs/VideoConference.tsx index 50ba23e95..429c384d6 100644 --- a/packages/react/src/prefabs/VideoConference.tsx +++ b/packages/react/src/prefabs/VideoConference.tsx @@ -125,7 +125,7 @@ export function VideoConference({ - {focusTrack && } + {focusTrack && }
)}