Skip to content

Commit

Permalink
Use trackRef as primary way of passing track info around, deprecate…
Browse files Browse the repository at this point in the history
… participant/source based APIs (#627)
  • Loading branch information
lukasIO authored Sep 7, 2023
1 parent ca88e63 commit 999eb2c
Show file tree
Hide file tree
Showing 30 changed files with 646 additions and 246 deletions.
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

0 comments on commit 999eb2c

Please sign in to comment.