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 && }
)}