Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use trackRef as primary way of passing track info around, deprecate participant/source based APIs #627

Merged
merged 1 commit into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/eight-nails-rush.md
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions .changeset/quiet-yaks-argue.md
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions .changeset/strong-plums-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@livekit/components-react': minor
'@livekit/component-example-next': patch
---

refactor `ParticipantTile` and `useParticipantTile` to trackRef and rename `TrackContext` to `TrackRefContext`.
5 changes: 5 additions & 0 deletions .changeset/warm-geese-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/components-react': minor
---

Update AudioTrack and VideoTrack components to accept track references.
4 changes: 2 additions & 2 deletions examples/nextjs/pages/clubhouse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
useIsMuted,
useIsSpeaking,
useToken,
useTrackContext,
useTrackRefContext,
useTracks,
} from '@livekit/components-react';
import styles from '../styles/Clubhouse.module.scss';
Expand Down Expand Up @@ -86,7 +86,7 @@ const Stage = () => {
};

const CustomParticipantTile = () => {
const { participant, source } = useTrackContext();
const { participant, source } = useTrackRefContext();
const isSpeaking = useIsSpeaking(participant);
const isMuted = useIsMuted(source);

Expand Down
6 changes: 3 additions & 3 deletions examples/nextjs/pages/customize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -78,7 +78,7 @@ export function Stage() {
<>
<div className={styles.participantGrid}>
<GridLayout tracks={tracks}>
<TrackContext.Consumer>
<TrackRefContext.Consumer>
{(track) =>
track && (
<div className="my-tile">
Expand All @@ -100,7 +100,7 @@ export function Stage() {
</div>
)
}
</TrackContext.Consumer>
</TrackRefContext.Consumer>
</GridLayout>
</div>
</>
Expand Down
6 changes: 4 additions & 2 deletions examples/nextjs/pages/simple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
LiveKitRoom,
ParticipantTile,
RoomName,
TrackContext,
TrackRefContext,
useToken,
useTracks,
} from '@livekit/components-react';
Expand Down Expand Up @@ -72,7 +72,9 @@ function Stage() {
<>
{screenShareTrack && <ParticipantTile {...screenShareTrack} />}
<GridLayout tracks={cameraTracks}>
<TrackContext.Consumer>{(track) => <ParticipantTile {...track} />}</TrackContext.Consumer>
<TrackRefContext.Consumer>
{(track) => <ParticipantTile {...track} />}
</TrackRefContext.Consumer>
</GridLayout>
</>
);
Expand Down
13 changes: 12 additions & 1 deletion packages/core/etc/components-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -501,6 +509,9 @@ export type TrackReference = {
// @public (undocumented)
export type TrackReferenceFilter = Parameters<TrackReferenceOrPlaceholder[]['filter']>['0'];

// @public (undocumented)
export type TrackReferenceId = ReturnType<typeof getTrackReferenceId>;

// @public (undocumented)
export type TrackReferenceOrPlaceholder = TrackReference | TrackReferencePlaceholder;

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/sorting/tile-array-update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)],
Expand Down
34 changes: 22 additions & 12 deletions packages/core/src/sorting/tile-array-update.ts
Original file line number Diff line number Diff line change
@@ -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<T> = {
Expand Down Expand Up @@ -80,12 +85,12 @@ export function updatePages<T extends UpdatableItem>(
): 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) => {
Expand Down Expand Up @@ -131,7 +136,7 @@ export function updatePages<T extends UpdatableItem>(

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)),
);
Expand All @@ -141,19 +146,24 @@ export function updatePages<T extends UpdatableItem>(
}

/**
* 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<T extends UpdatableItem>(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;
});
}
38 changes: 36 additions & 2 deletions packages/core/src/track-reference/test-utils.test.ts
Original file line number Diff line number Diff line change
@@ -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.', () => {
Expand All @@ -23,3 +24,36 @@ describe('Test mocking functions ', () => {
expectTypeOf(mock.source).toMatchTypeOf<Track.Source>();
});
});

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);
});
});
2 changes: 1 addition & 1 deletion packages/core/src/track-reference/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
58 changes: 58 additions & 0 deletions packages/core/src/track-reference/track-reference.utils.test.ts
Original file line number Diff line number Diff line change
@@ -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<Participant>();

// Check if the publication is mocked correctly:
expect(mock.publication).toBeDefined();
expect(mock.publication.kind).toBe(Track.Kind.Video);
expectTypeOf(mock.publication).toMatchTypeOf<TrackPublication>();

// Check if the source is mocked correctly:
expect(mock.source).toBeDefined();
expect(mock.source).toBe(Track.Source.Camera);
expectTypeOf(mock.source).toMatchTypeOf<Track.Source>();
});
});

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);
},
);
});
46 changes: 39 additions & 7 deletions packages/core/src/track-reference/track-reference.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof getTrackReferenceId>;

/** Returns the Source of the TrackReference. */
export function getTrackReferenceSource(trackReference: TrackReferenceOrPlaceholder): Track.Source {
if (isTrackReference(trackReference)) {
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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
);
}
Loading