UNPKG

@livekit/components-core

Version:
1 lines 161 kB
{"version":3,"sources":["../src/constants.ts","../src/track-reference/track-reference.types.ts","../src/track-reference/track-reference.utils.ts","../src/utils.ts","../src/helper/detectMobileBrowser.ts","../src/helper/url-regex.ts","../src/helper/emailRegex.ts","../src/helper/floating-menu.ts","../src/helper/tokenizer.ts","../src/helper/eventGroups.ts","../src/logger.ts","../src/helper/grid-layouts.ts","../src/helper/set-helper.ts","../src/helper/featureDetection.ts","../src/helper/transcriptions.ts","../src/helper/participant-attributes.ts","../src/types.ts","../src/sorting/sort-track-bundles.ts","../src/sorting/base-sort-functions.ts","../src/sorting/sort-participants.ts","../src/helper/array-helper.ts","../src/track-reference/test-utils.ts","../src/sorting/tile-array-update.ts","../src/components/mediaToggle.ts","../src/observables/participant.ts","../src/components/mediaTrack.ts","../src/styles-interface/class-prefixer.ts","../src/observables/room.ts","../src/components/mediaDeviceSelect.ts","../src/components/disconnectButton.ts","../src/components/connectionQualityIndicator.ts","../src/components/trackMutedIndicator.ts","../src/components/participantName.ts","../src/components/participantTile.ts","../src/components/chat.ts","../src/observables/dataChannel.ts","../src/helper/future.ts","../src/components/startAudio.ts","../src/components/startVideo.ts","../src/components/chatToggle.ts","../src/components/focusToggle.ts","../src/components/clearPinButton.ts","../src/components/room.ts","../src/observables/track.ts","../src/observables/dom-event.ts","../src/persistent-storage/local-storage-helpers.ts","../src/persistent-storage/user-choices.ts","../src/components/textStream.ts"],"sourcesContent":["export const cssPrefix = 'lk';\n","/**\n * The TrackReference type is a logical grouping of participant publication and/or subscribed track.\n *\n */\n\nimport type { Participant, Track, TrackPublication } from 'livekit-client';\n// ## TrackReference Types\n\n/** @public */\nexport type TrackReferencePlaceholder = {\n participant: Participant;\n publication?: never;\n source: Track.Source;\n};\n\n/** @public */\nexport type TrackReference = {\n participant: Participant;\n publication: TrackPublication;\n source: Track.Source;\n};\n\n/** @public */\nexport type TrackReferenceOrPlaceholder = TrackReference | TrackReferencePlaceholder;\n\n// ### TrackReference Type Predicates\n/** @internal */\nexport function isTrackReference(trackReference: unknown): trackReference is TrackReference {\n if (typeof trackReference === 'undefined') {\n return false;\n }\n return (\n isTrackReferenceSubscribed(trackReference as TrackReference) ||\n isTrackReferencePublished(trackReference as TrackReference)\n );\n}\n\nfunction isTrackReferenceSubscribed(trackReference?: TrackReferenceOrPlaceholder): boolean {\n if (!trackReference) {\n return false;\n }\n return (\n trackReference.hasOwnProperty('participant') &&\n trackReference.hasOwnProperty('source') &&\n trackReference.hasOwnProperty('track') &&\n typeof trackReference.publication?.track !== 'undefined'\n );\n}\n\nfunction isTrackReferencePublished(trackReference?: TrackReferenceOrPlaceholder): boolean {\n if (!trackReference) {\n return false;\n }\n return (\n trackReference.hasOwnProperty('participant') &&\n trackReference.hasOwnProperty('source') &&\n trackReference.hasOwnProperty('publication') &&\n typeof trackReference.publication !== 'undefined'\n );\n}\n\nexport function isTrackReferencePlaceholder(\n trackReference?: TrackReferenceOrPlaceholder,\n): trackReference is TrackReferencePlaceholder {\n if (!trackReference) {\n return false;\n }\n return (\n trackReference.hasOwnProperty('participant') &&\n trackReference.hasOwnProperty('source') &&\n typeof trackReference.publication === 'undefined'\n );\n}\n","import type { Track } from 'livekit-client';\nimport type { PinState } from '../types';\nimport type { TrackReferenceOrPlaceholder } from './track-reference.types';\nimport { isTrackReference, isTrackReferencePlaceholder } from './track-reference.types';\n\n/**\n * Returns a id to identify the `TrackReference` or `TrackReferencePlaceholder` based on\n * participant, track source and trackSid.\n * @remarks\n * The id pattern is: `${participantIdentity}_${trackSource}_${trackSid}` for `TrackReference`\n * and `${participantIdentity}_${trackSource}_placeholder` for `TrackReferencePlaceholder`.\n */\nexport function getTrackReferenceId(trackReference: TrackReferenceOrPlaceholder | number) {\n if (typeof trackReference === 'string' || typeof trackReference === 'number') {\n return `${trackReference}`;\n } else if (isTrackReferencePlaceholder(trackReference)) {\n return `${trackReference.participant.identity}_${trackReference.source}_placeholder`;\n } else if (isTrackReference(trackReference)) {\n return `${trackReference.participant.identity}_${trackReference.publication.source}_${trackReference.publication.trackSid}`;\n } else {\n throw new Error(`Can't generate a id for the given track reference: ${trackReference}`);\n }\n}\n\nexport type TrackReferenceId = ReturnType<typeof getTrackReferenceId>;\n\n/** Returns the Source of the TrackReference. */\nexport function getTrackReferenceSource(trackReference: TrackReferenceOrPlaceholder): Track.Source {\n if (isTrackReference(trackReference)) {\n return trackReference.publication.source;\n } else {\n return trackReference.source;\n }\n}\n\nexport function isEqualTrackRef(\n a?: TrackReferenceOrPlaceholder,\n b?: TrackReferenceOrPlaceholder,\n): boolean {\n if (a === undefined || b === undefined) {\n return false;\n }\n if (isTrackReference(a) && isTrackReference(b)) {\n return a.publication.trackSid === b.publication.trackSid;\n } else {\n return getTrackReferenceId(a) === getTrackReferenceId(b);\n }\n}\n\n/**\n * Check if the `TrackReference` is pinned.\n */\nexport function isTrackReferencePinned(\n trackReference: TrackReferenceOrPlaceholder,\n pinState: PinState | undefined,\n): boolean {\n if (typeof pinState === 'undefined') {\n return false;\n }\n if (isTrackReference(trackReference)) {\n return pinState.some(\n (pinnedTrackReference) =>\n pinnedTrackReference.participant.identity === trackReference.participant.identity &&\n isTrackReference(pinnedTrackReference) &&\n pinnedTrackReference.publication.trackSid === trackReference.publication.trackSid,\n );\n } else if (isTrackReferencePlaceholder(trackReference)) {\n return pinState.some(\n (pinnedTrackReference) =>\n pinnedTrackReference.participant.identity === trackReference.participant.identity &&\n isTrackReferencePlaceholder(pinnedTrackReference) &&\n pinnedTrackReference.source === trackReference.source,\n );\n } else {\n return false;\n }\n}\n\n/**\n * Check if the current `currentTrackRef` is the placeholder for next `nextTrackRef`.\n * Based on the participant identity and the source.\n * @internal\n */\nexport function isPlaceholderReplacement(\n currentTrackRef: TrackReferenceOrPlaceholder,\n nextTrackRef: TrackReferenceOrPlaceholder,\n) {\n // if (typeof nextTrackRef === 'number' || typeof currentTrackRef === 'number') {\n // return false;\n // }\n return (\n isTrackReferencePlaceholder(currentTrackRef) &&\n isTrackReference(nextTrackRef) &&\n nextTrackRef.participant.identity === currentTrackRef.participant.identity &&\n nextTrackRef.source === currentTrackRef.source\n );\n}\n","import type { Participant, TrackPublication } from 'livekit-client';\n\nimport type { TrackReference } from './track-reference';\nimport { isEqualTrackRef } from './track-reference';\nimport type { PinState } from './types';\n\nexport function isLocal(p: Participant) {\n return p.isLocal;\n}\n\nexport function isRemote(p: Participant) {\n return !p.isLocal;\n}\n\nexport const attachIfSubscribed = (\n publication: TrackPublication | undefined,\n element: HTMLMediaElement | null | undefined,\n) => {\n if (!publication) return;\n const { isSubscribed, track } = publication;\n if (element && track) {\n if (isSubscribed) {\n track.attach(element);\n } else {\n track.detach(element);\n }\n }\n};\n\n/**\n * Check if the participant track reference is pinned.\n */\nexport function isParticipantTrackReferencePinned(\n trackRef: TrackReference,\n pinState: PinState | undefined,\n): boolean {\n if (pinState === undefined) {\n return false;\n }\n\n return pinState.some((pinnedTrackRef) => isEqualTrackRef(pinnedTrackRef, trackRef));\n}\n\n/**\n * Calculates the scrollbar width by creating two HTML elements\n * and messaging the difference.\n * @internal\n */\nexport function getScrollBarWidth() {\n const inner = document.createElement('p');\n inner.style.width = '100%';\n inner.style.height = '200px';\n\n const outer = document.createElement('div');\n outer.style.position = 'absolute';\n outer.style.top = '0px';\n outer.style.left = '0px';\n outer.style.visibility = 'hidden';\n outer.style.width = '200px';\n outer.style.height = '150px';\n outer.style.overflow = 'hidden';\n outer.appendChild(inner);\n\n document.body.appendChild(outer);\n const w1 = inner.offsetWidth;\n outer.style.overflow = 'scroll';\n let w2 = inner.offsetWidth;\n if (w1 === w2) {\n w2 = outer.clientWidth;\n }\n document.body.removeChild(outer);\n const scrollBarWidth = w1 - w2;\n return scrollBarWidth;\n}\n","/**\n * @internal\n */\nexport function isWeb(): boolean {\n return typeof document !== 'undefined';\n}\n\n/**\n * Mobile browser detection based on `navigator.userAgent` string.\n * Defaults to returning `false` if not in a browser.\n *\n * @remarks\n * This should only be used if feature detection or other methods do not work!\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#mobile_device_detection\n */\nexport function isMobileBrowser(): boolean {\n return isWeb() ? /Mobi/i.test(window.navigator.userAgent) : false;\n}\n","// The MIT License (MIT)\n\n// Copyright (c) Kevin Mårtensson <kevinmartensson@gmail.com> and Diego Perini\n\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n\n// The above copyright notice and this permission notice shall be included in\n// all copies or substantial portions of the Software.\n\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n// THE SOFTWARE.\n\ninterface RegExOptions {\n /**\n\t\tOnly match an exact string. Useful with `RegExp#test` to check if a string is a URL.\n\t\t@defaultValue false\n\t\t*/\n readonly exact?: boolean;\n}\n\nexport function createUrlRegExp(options: RegExOptions) {\n options = {\n ...options,\n };\n\n const protocol = `(?:(?:[a-z]+:)?//)?`;\n const auth = '(?:\\\\S+(?::\\\\S*)?@)?';\n const ip = new RegExp(\n '(?:25[0-5]|2[0-4]\\\\d|1\\\\d\\\\d|[1-9]\\\\d|\\\\d)(?:\\\\.(?:25[0-5]|2[0-4]\\\\d|1\\\\d\\\\d|[1-9]\\\\d|\\\\d)){3}',\n 'g',\n ).source;\n const host = '(?:(?:[a-z\\\\u00a1-\\\\uffff0-9][-_]*)*[a-z\\\\u00a1-\\\\uffff0-9]+)';\n const domain = '(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff0-9]-*)*[a-z\\\\u00a1-\\\\uffff0-9]+)*';\n const tld = `(?:\\\\.(?:[a-z\\\\u00a1-\\\\uffff]{2,}))\\\\.?`;\n const port = '(?::\\\\d{2,5})?';\n const path = '(?:[/?#][^\\\\s\"]*)?';\n const regex = `(?:${protocol}|www\\\\.)${auth}(?:localhost|${ip}|${host}${domain}${tld})${port}${path}`;\n\n return options.exact ? new RegExp(`(?:^${regex}$)`, 'i') : new RegExp(regex, 'ig');\n}\n","// source code adapted from https://github.com/sindresorhus/email-regex due to ESM import incompatibilities when trying to serve a CJS version of components\n\nconst regex = '[^\\\\.\\\\s@:](?:[^\\\\s@:]*[^\\\\s@:\\\\.])?@[^\\\\.\\\\s@]+(?:\\\\.[^\\\\.\\\\s@]+)*';\n\nfunction createEmailRegExp({ exact }: { exact?: boolean } = {}) {\n return exact ? new RegExp(`^${regex}$`) : new RegExp(regex, 'g');\n}\nexport { createEmailRegExp };\n","import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';\n\nexport function computeMenuPosition(\n button: HTMLElement,\n menu: HTMLElement,\n onUpdate?: (x: number, y: number) => void,\n): () => void {\n const cleanup = autoUpdate(button, menu, async () => {\n const { x, y } = await computePosition(button, menu, {\n placement: 'top',\n middleware: [offset(6), flip(), shift({ padding: 5 })],\n });\n\n onUpdate?.(x, y);\n });\n return cleanup;\n}\n\nexport function wasClickOutside(insideElement: HTMLElement, event: MouseEvent): boolean {\n const isOutside = !insideElement.contains(event.target as Node);\n return isOutside;\n}\n","import { createEmailRegExp } from './emailRegex';\nimport { createUrlRegExp } from './url-regex';\n\nexport type TokenizeGrammar = { [type: string]: RegExp };\n\nexport const createDefaultGrammar = () => {\n return {\n email: createEmailRegExp(),\n url: createUrlRegExp({}),\n } satisfies TokenizeGrammar;\n};\n\nexport function tokenize<T extends TokenizeGrammar>(input: string, grammar: T) {\n const matches = Object.entries(grammar)\n .map(([type, rx], weight) =>\n Array.from(input.matchAll(rx)).map(({ index, 0: content }) => ({\n type: type as keyof T,\n weight,\n content,\n index: index ?? 0,\n })),\n )\n .flat()\n .sort((a, b) => {\n const d = a.index - b.index;\n return d !== 0 ? d : a.weight - b.weight;\n })\n .filter(({ index }, i, arr) => {\n if (i === 0) return true;\n const prev = arr[i - 1];\n return prev.index + prev.content.length <= index;\n });\n\n const tokens = [];\n let pos = 0;\n for (const { type, content, index } of matches) {\n if (index > pos) tokens.push(input.substring(pos, index));\n tokens.push({ type, content });\n pos = index + content.length;\n }\n if (input.length > pos) tokens.push(input.substring(pos));\n return tokens;\n}\n","import { ParticipantEvent, RoomEvent } from 'livekit-client';\n\nexport const allRemoteParticipantRoomEvents = [\n RoomEvent.ConnectionStateChanged,\n RoomEvent.RoomMetadataChanged,\n\n RoomEvent.ActiveSpeakersChanged,\n RoomEvent.ConnectionQualityChanged,\n\n RoomEvent.ParticipantConnected,\n RoomEvent.ParticipantDisconnected,\n RoomEvent.ParticipantPermissionsChanged,\n RoomEvent.ParticipantMetadataChanged,\n RoomEvent.ParticipantNameChanged,\n RoomEvent.ParticipantAttributesChanged,\n\n RoomEvent.TrackMuted,\n RoomEvent.TrackUnmuted,\n RoomEvent.TrackPublished,\n RoomEvent.TrackUnpublished,\n RoomEvent.TrackStreamStateChanged,\n RoomEvent.TrackSubscriptionFailed,\n RoomEvent.TrackSubscriptionPermissionChanged,\n RoomEvent.TrackSubscriptionStatusChanged,\n];\n\nexport const allParticipantRoomEvents = [\n ...allRemoteParticipantRoomEvents,\n RoomEvent.LocalTrackPublished,\n RoomEvent.LocalTrackUnpublished,\n];\n\nexport const participantTrackEvents = [\n ParticipantEvent.TrackPublished,\n ParticipantEvent.TrackUnpublished,\n ParticipantEvent.TrackMuted,\n ParticipantEvent.TrackUnmuted,\n ParticipantEvent.TrackStreamStateChanged,\n ParticipantEvent.TrackSubscribed,\n ParticipantEvent.TrackUnsubscribed,\n ParticipantEvent.TrackSubscriptionPermissionChanged,\n ParticipantEvent.TrackSubscriptionFailed,\n ParticipantEvent.LocalTrackPublished,\n ParticipantEvent.LocalTrackUnpublished,\n];\n\nexport const allRemoteParticipantEvents = [\n ParticipantEvent.ConnectionQualityChanged,\n ParticipantEvent.IsSpeakingChanged,\n ParticipantEvent.ParticipantMetadataChanged,\n ParticipantEvent.ParticipantPermissionsChanged,\n\n ParticipantEvent.TrackMuted,\n ParticipantEvent.TrackUnmuted,\n ParticipantEvent.TrackPublished,\n ParticipantEvent.TrackUnpublished,\n ParticipantEvent.TrackStreamStateChanged,\n ParticipantEvent.TrackSubscriptionFailed,\n ParticipantEvent.TrackSubscriptionPermissionChanged,\n ParticipantEvent.TrackSubscriptionStatusChanged,\n];\n\nexport const allParticipantEvents = [\n ...allRemoteParticipantEvents,\n ParticipantEvent.LocalTrackPublished,\n ParticipantEvent.LocalTrackUnpublished,\n];\n","import {\n setLogLevel as setClientSdkLogLevel,\n setLogExtension as setClientSdkLogExtension,\n LogLevel as LogLevelEnum,\n} from 'livekit-client';\nimport loglevel from 'loglevel';\n\nexport const log = loglevel.getLogger('lk-components-js');\nlog.setDefaultLevel('WARN');\n\ntype LogLevel = Parameters<typeof setClientSdkLogLevel>[0];\ntype SetLogLevelOptions = {\n liveKitClientLogLevel?: LogLevel;\n};\n\n/**\n * Set the log level for both the `@livekit/components-react` package and the `@livekit-client` package.\n * To set the `@livekit-client` log independently, use the `liveKitClientLogLevel` prop on the `options` object.\n * @public\n */\nexport function setLogLevel(level: LogLevel, options: SetLogLevelOptions = {}): void {\n log.setLevel(level);\n setClientSdkLogLevel(options.liveKitClientLogLevel ?? level);\n}\n\ntype LogExtension = (level: LogLevel, msg: string, context?: object) => void;\ntype SetLogExtensionOptions = {\n liveKitClientLogExtension?: LogExtension;\n};\n\n/**\n * Set the log extension for both the `@livekit/components-react` package and the `@livekit-client` package.\n * To set the `@livekit-client` log extension, use the `liveKitClientLogExtension` prop on the `options` object.\n * @public\n */\nexport function setLogExtension(extension: LogExtension, options: SetLogExtensionOptions = {}) {\n const originalFactory = log.methodFactory;\n\n log.methodFactory = (methodName, configLevel, loggerName) => {\n const rawMethod = originalFactory(methodName, configLevel, loggerName);\n\n const logLevel = LogLevelEnum[methodName];\n const needLog = logLevel >= configLevel && logLevel < LogLevelEnum.silent;\n\n return (msg, context?: [msg: string, context: object]) => {\n if (context) rawMethod(msg, context);\n else rawMethod(msg);\n if (needLog) {\n extension(logLevel, msg, context);\n }\n };\n };\n log.setLevel(log.getLevel()); // Be sure to call setLevel method in order to apply plugin\n setClientSdkLogExtension(options.liveKitClientLogExtension ?? extension);\n}\n","import { log } from '../logger';\n\n/**\n * @public\n */\nexport type GridLayoutDefinition = {\n /** Column count of the grid layout. */\n columns: number;\n /** Row count of the grid layout. */\n rows: number;\n // # Constraints that have to be meet to use this layout.\n /**\n * Minimum grid container width required to use this layout.\n * @remarks\n * If this constraint is not met, we try to select a layout with fewer tiles\n * (`tiles=columns*rows`) that is within the constraint.\n */\n minWidth?: number;\n /**\n * Minimum grid container height required to use this layout.\n * @remarks\n * If this constraint is not met, we try to select a layout with fewer tiles\n * (`tiles=columns*rows`) that is within the constraint.\n */\n minHeight?: number;\n /**\n * For which orientation the layout definition should be applied.\n * Will be used for both landscape and portrait if no value is specified.\n */\n orientation?: 'landscape' | 'portrait';\n};\n\nexport type GridLayoutInfo = {\n /** Layout name (convention `<column_count>x<row_count>`). */\n name: string;\n /** Column count of the layout. */\n columns: number;\n /** Row count of the layout. */\n rows: number;\n // # Constraints that have to be meet to use this layout.\n // ## 1. Participant range:\n /** Maximum tiles that fit into this layout. */\n maxTiles: number;\n // ## 2. Screen size limits:\n /** Minimum width required to use this layout. */\n minWidth: number;\n /** Minimum height required to use this layout. */\n minHeight: number;\n orientation?: 'landscape' | 'portrait';\n};\n\nexport const GRID_LAYOUTS: GridLayoutDefinition[] = [\n {\n columns: 1,\n rows: 1,\n },\n {\n columns: 1,\n rows: 2,\n orientation: 'portrait',\n },\n {\n columns: 2,\n rows: 1,\n orientation: 'landscape',\n },\n {\n columns: 2,\n rows: 2,\n minWidth: 560,\n },\n {\n columns: 3,\n rows: 3,\n minWidth: 700,\n },\n {\n columns: 4,\n rows: 4,\n minWidth: 960,\n },\n {\n columns: 5,\n rows: 5,\n minWidth: 1100,\n },\n] as const;\n\nexport function selectGridLayout(\n layoutDefinitions: GridLayoutDefinition[],\n participantCount: number,\n width: number,\n height: number,\n): GridLayoutInfo {\n if (layoutDefinitions.length < 1) {\n throw new Error('At least one grid layout definition must be provided.');\n }\n const layouts = expandAndSortLayoutDefinitions(layoutDefinitions);\n if (width <= 0 || height <= 0) {\n return layouts[0];\n }\n // Find the best layout to fit all participants.\n let currentLayoutIndex = 0;\n const containerOrientation = width / height > 1 ? 'landscape' : 'portrait';\n let layout = layouts.find((layout_, index, allLayouts) => {\n currentLayoutIndex = index;\n const isBiggerLayoutAvailable =\n allLayouts.findIndex((l, i) => {\n const fitsOrientation = !l.orientation || l.orientation === containerOrientation;\n const layoutIsBiggerThanCurrent = i > index;\n const layoutFitsSameAmountOfParticipants = l.maxTiles === layout_.maxTiles;\n return layoutIsBiggerThanCurrent && layoutFitsSameAmountOfParticipants && fitsOrientation;\n }) !== -1;\n return layout_.maxTiles >= participantCount && !isBiggerLayoutAvailable;\n });\n if (layout === undefined) {\n layout = layouts[layouts.length - 1];\n if (layout) {\n log.warn(\n `No layout found for: participantCount: ${participantCount}, width/height: ${width}/${height} fallback to biggest available layout (${layout}).`,\n );\n } else {\n throw new Error(`No layout or fallback layout found.`);\n }\n }\n\n // Check if the layout fits into the screen constraints. If not, recursively check the next smaller layout.\n if (width < layout.minWidth || height < layout.minHeight) {\n // const currentLayoutIndex = layouts.indexOf(layout);\n if (currentLayoutIndex > 0) {\n const smallerLayout = layouts[currentLayoutIndex - 1];\n layout = selectGridLayout(\n layouts.slice(0, currentLayoutIndex),\n smallerLayout.maxTiles,\n width,\n height,\n );\n }\n }\n return layout;\n}\n\n/**\n * @internal\n */\nexport function expandAndSortLayoutDefinitions(layouts: GridLayoutDefinition[]): GridLayoutInfo[] {\n return [...layouts]\n .map((layout) => {\n return {\n name: `${layout.columns}x${layout.rows}`,\n columns: layout.columns,\n rows: layout.rows,\n maxTiles: layout.columns * layout.rows,\n minWidth: layout.minWidth ?? 0,\n minHeight: layout.minHeight ?? 0,\n orientation: layout.orientation,\n } satisfies GridLayoutInfo;\n })\n .sort((a, b) => {\n if (a.maxTiles !== b.maxTiles) {\n return a.maxTiles - b.maxTiles;\n } else if (a.minWidth !== 0 || b.minWidth !== 0) {\n return a.minWidth - b.minWidth;\n } else if (a.minHeight !== 0 || b.minHeight !== 0) {\n return a.minHeight - b.minHeight;\n } else {\n return 0;\n }\n });\n}\n","export function setDifference<T>(setA: Set<T>, setB: Set<T>): Set<T> {\n const _difference = new Set(setA);\n for (const elem of setB) {\n _difference.delete(elem);\n }\n return _difference;\n}\n","/**\n * Returns `true` if the browser supports screen sharing.\n */\nexport function supportsScreenSharing(): boolean {\n return (\n typeof navigator !== 'undefined' &&\n navigator.mediaDevices &&\n !!navigator.mediaDevices.getDisplayMedia\n );\n}\n","import type { TranscriptionSegment } from 'livekit-client';\n\nexport type ReceivedTranscriptionSegment = TranscriptionSegment & {\n receivedAtMediaTimestamp: number;\n receivedAt: number;\n};\n\nexport function getActiveTranscriptionSegments(\n segments: ReceivedTranscriptionSegment[],\n syncTimes: { timestamp: number; rtpTimestamp?: number },\n maxAge = 0,\n) {\n return segments.filter((segment) => {\n const hasTrackSync = !!syncTimes.rtpTimestamp;\n const currentTrackTime = syncTimes.rtpTimestamp ?? performance.timeOrigin + performance.now();\n // if a segment arrives late, consider startTime to be the media timestamp from when the segment was received client side\n const displayStartTime = hasTrackSync\n ? Math.max(segment.receivedAtMediaTimestamp, segment.startTime)\n : segment.receivedAt;\n // \"active\" duration is computed by the diff between start and end time, so we don't rely on displayStartTime to always be the same as the segment's startTime\n const segmentDuration = maxAge + segment.endTime - segment.startTime;\n return (\n currentTrackTime >= displayStartTime && currentTrackTime <= displayStartTime + segmentDuration\n );\n });\n}\n\nexport function addMediaTimestampToTranscription(\n segment: TranscriptionSegment,\n timestamps: { timestamp: number; rtpTimestamp?: number },\n): ReceivedTranscriptionSegment {\n return {\n ...segment,\n receivedAtMediaTimestamp: timestamps.rtpTimestamp ?? 0,\n receivedAt: timestamps.timestamp,\n };\n}\n\n/**\n * @returns An array of unique (by id) `TranscriptionSegment`s. Latest wins. If the resulting array would be longer than `windowSize`, the array will be reduced to `windowSize` length\n */\nexport function dedupeSegments<T extends TranscriptionSegment>(\n prevSegments: T[],\n newSegments: T[],\n windowSize: number,\n) {\n return [...prevSegments, ...newSegments]\n .reduceRight((acc, segment) => {\n if (!acc.find((val) => val.id === segment.id)) {\n acc.unshift(segment);\n }\n return acc;\n }, [] as Array<T>)\n .slice(0 - windowSize);\n}\n\nexport function didActiveSegmentsChange<T extends TranscriptionSegment>(\n prevActive: T[],\n newActive: T[],\n) {\n if (newActive.length !== prevActive.length) {\n return true;\n }\n return !newActive.every((newSegment) => {\n return prevActive.find(\n (prevSegment) =>\n prevSegment.id === newSegment.id &&\n prevSegment.text === newSegment.text &&\n prevSegment.final === newSegment.final &&\n prevSegment.language === newSegment.language &&\n prevSegment.startTime === newSegment.startTime &&\n prevSegment.endTime === newSegment.endTime,\n );\n });\n}\n","/** An enum of first party livekit attributes generated by the serverside agents sdk */\nexport enum ParticipantAgentAttributes {\n AgentState = 'lk.agent.state',\n PublishOnBehalf = 'lk.publish_on_behalf',\n\n TranscriptionFinal = 'lk.transcription_final',\n TranscriptionSegmentId = 'lk.segment_id',\n TranscribedTrackId = 'lk.transcribed_track_id',\n\n Expression = 'lk.expression',\n}\n","import type { Participant, ParticipantKind, Track, TrackPublication } from 'livekit-client';\nimport type { TrackReference, TrackReferenceOrPlaceholder } from './track-reference';\n\n// ## PinState Type\n/** @public */\nexport type PinState = TrackReferenceOrPlaceholder[];\nexport const PIN_DEFAULT_STATE: PinState = [];\n\n// ## WidgetState Types\n/** @public */\nexport type WidgetState = {\n showChat: boolean;\n unreadMessages: number;\n showSettings?: boolean;\n};\nexport const WIDGET_DEFAULT_STATE: WidgetState = {\n showChat: false,\n unreadMessages: 0,\n showSettings: false,\n};\n\n// ## Track Source Types\nexport type TrackSourceWithOptions = { source: Track.Source; withPlaceholder: boolean };\n\nexport type SourcesArray = Track.Source[] | TrackSourceWithOptions[];\n\n// ### Track Source Type Predicates\nexport function isSourceWitOptions(source: SourcesArray[number]): source is TrackSourceWithOptions {\n return typeof source === 'object';\n}\n\nexport function isSourcesWithOptions(sources: SourcesArray): sources is TrackSourceWithOptions[] {\n return (\n Array.isArray(sources) &&\n (sources as TrackSourceWithOptions[]).filter(isSourceWitOptions).length > 0\n );\n}\n\n// ## Loop Filter Types\nexport type TrackReferenceFilter = Parameters<TrackReferenceOrPlaceholder[]['filter']>['0'];\nexport type ParticipantFilter = Parameters<Participant[]['filter']>['0'];\n\n// ## Other Types\n/** @internal */\nexport interface ParticipantClickEvent {\n participant: Participant;\n track?: TrackPublication;\n}\n\nexport type TrackSource<T extends Track.Source> = RequireAtLeastOne<\n { source: T; name: string; participant: Participant },\n 'name' | 'source'\n>;\n\nexport type ParticipantTrackIdentifier = RequireAtLeastOne<\n { sources: Track.Source[]; name: string; kind: Track.Kind },\n 'sources' | 'name' | 'kind'\n>;\n\n/**\n * @beta\n */\nexport type ParticipantIdentifier = RequireAtLeastOne<\n { kind: ParticipantKind; identity: string },\n 'identity' | 'kind'\n>;\n\n/**\n * The TrackIdentifier type is used to select Tracks either based on\n * - Track.Source and/or name of the track, e.g. `{source: Track.Source.Camera}` or `{name: \"my-track\"}`\n * - TrackReference (participant and publication)\n * @internal\n */\nexport type TrackIdentifier<T extends Track.Source = Track.Source> =\n TrackSource<T> | TrackReference;\n\n// ## Util Types\ntype RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> &\n {\n [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>;\n }[Keys];\n\nexport type RequireOnlyOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> &\n {\n [K in Keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<Keys, K>, undefined>>;\n }[Keys];\n\nexport type AudioSource = Track.Source.Microphone | Track.Source.ScreenShareAudio;\nexport type VideoSource = Track.Source.Camera | Track.Source.ScreenShare;\n","import { Track } from 'livekit-client';\nimport type { TrackReferenceOrPlaceholder } from '../track-reference';\nimport { isTrackReference } from '../track-reference';\nimport {\n sortParticipantsByAudioLevel,\n sortParticipantsByIsSpeaking,\n sortParticipantsByJoinedAt,\n sortParticipantsByLastSpokenAT,\n sortTrackReferencesByType,\n sortTrackRefsByIsCameraEnabled,\n} from './base-sort-functions';\n\n/**\n * Default sort for `TrackReferenceOrPlaceholder`, it'll order participants by:\n * 1. local camera track (publication.isLocal)\n * 2. remote screen_share track\n * 3. local screen_share track\n * 4. remote dominant speaker camera track (sorted by speaker with the loudest audio level)\n * 5. other remote speakers that are recently active\n * 6. remote unmuted camera tracks\n * 7. remote tracks sorted by joinedAt\n */\nexport function sortTrackReferences(\n tracks: TrackReferenceOrPlaceholder[],\n): TrackReferenceOrPlaceholder[] {\n const localTracks: TrackReferenceOrPlaceholder[] = [];\n const screenShareTracks: TrackReferenceOrPlaceholder[] = [];\n const cameraTracks: TrackReferenceOrPlaceholder[] = [];\n const undefinedTracks: TrackReferenceOrPlaceholder[] = [];\n\n tracks.forEach((trackRef) => {\n if (trackRef.participant.isLocal && trackRef.source === Track.Source.Camera) {\n localTracks.push(trackRef);\n } else if (trackRef.source === Track.Source.ScreenShare) {\n screenShareTracks.push(trackRef);\n } else if (trackRef.source === Track.Source.Camera) {\n cameraTracks.push(trackRef);\n } else {\n undefinedTracks.push(trackRef);\n }\n });\n\n const sortedScreenShareTracks = sortScreenShareTracks(screenShareTracks);\n const sortedCameraTracks = sortCameraTracks(cameraTracks);\n\n return [...localTracks, ...sortedScreenShareTracks, ...sortedCameraTracks, ...undefinedTracks];\n}\n\n/**\n * Sort an array of `TrackReference` screen shares.\n * Main sorting order:\n * 1. remote screen shares\n * 2. local screen shares\n * Secondary sorting by participant's joining time.\n */\nfunction sortScreenShareTracks(\n screenShareTracks: TrackReferenceOrPlaceholder[],\n): TrackReferenceOrPlaceholder[] {\n const localScreenShares: TrackReferenceOrPlaceholder[] = [];\n const remoteScreenShares: TrackReferenceOrPlaceholder[] = [];\n\n screenShareTracks.forEach((trackRef) => {\n if (trackRef.participant.isLocal) {\n localScreenShares.push(trackRef);\n } else {\n remoteScreenShares.push(trackRef);\n }\n });\n\n localScreenShares.sort((a, b) => sortParticipantsByJoinedAt(a.participant, b.participant));\n remoteScreenShares.sort((a, b) => sortParticipantsByJoinedAt(a.participant, b.participant));\n\n const sortedScreenShareTrackRefs = [...remoteScreenShares, ...localScreenShares];\n return sortedScreenShareTrackRefs;\n}\n\nfunction sortCameraTracks(\n cameraTrackReferences: TrackReferenceOrPlaceholder[],\n): TrackReferenceOrPlaceholder[] {\n const localCameraTracks: TrackReferenceOrPlaceholder[] = [];\n const remoteCameraTracks: TrackReferenceOrPlaceholder[] = [];\n\n cameraTrackReferences.forEach((trackRef) => {\n if (trackRef.participant.isLocal) {\n localCameraTracks.push(trackRef);\n } else {\n remoteCameraTracks.push(trackRef);\n }\n });\n\n remoteCameraTracks.sort((a, b) => {\n // Participant with higher audio level goes first.\n if (a.participant.isSpeaking && b.participant.isSpeaking) {\n return sortParticipantsByAudioLevel(a.participant, b.participant);\n }\n\n // A speaking participant goes before one that is not speaking.\n if (a.participant.isSpeaking !== b.participant.isSpeaking) {\n return sortParticipantsByIsSpeaking(a.participant, b.participant);\n }\n\n // A participant that spoke recently goes before a participant that spoke a while back.\n if (a.participant.lastSpokeAt !== b.participant.lastSpokeAt) {\n return sortParticipantsByLastSpokenAT(a.participant, b.participant);\n }\n\n // TrackReference before TrackReferencePlaceholder\n if (isTrackReference(a) !== isTrackReference(b)) {\n return sortTrackReferencesByType(a, b);\n }\n\n // Tiles with video on before tiles with muted video track.\n if (a.participant.isCameraEnabled !== b.participant.isCameraEnabled) {\n return sortTrackRefsByIsCameraEnabled(a, b);\n }\n\n // A participant that joined a long time ago goes before one that joined recently.\n return sortParticipantsByJoinedAt(a.participant, b.participant);\n });\n\n return [...localCameraTracks, ...remoteCameraTracks];\n}\n","import type { Participant } from 'livekit-client';\nimport { Track } from 'livekit-client';\nimport type { TrackReferenceOrPlaceholder } from '../track-reference';\nimport { getTrackReferenceSource, isTrackReference } from '../track-reference';\n\nexport function sortParticipantsByAudioLevel(\n a: Pick<Participant, 'audioLevel'>,\n b: Pick<Participant, 'audioLevel'>,\n): number {\n return b.audioLevel - a.audioLevel;\n}\n\nexport function sortParticipantsByIsSpeaking(\n a: Pick<Participant, 'isSpeaking'>,\n b: Pick<Participant, 'isSpeaking'>,\n): number {\n if (a.isSpeaking === b.isSpeaking) {\n return 0;\n } else {\n return a.isSpeaking ? -1 : 1;\n }\n}\n\nexport function sortParticipantsByLastSpokenAT(\n a: Pick<Participant, 'lastSpokeAt'>,\n b: Pick<Participant, 'lastSpokeAt'>,\n): number {\n if (a.lastSpokeAt !== undefined || b.lastSpokeAt !== undefined) {\n return (b.lastSpokeAt?.getTime() ?? 0) - (a.lastSpokeAt?.getTime() ?? 0);\n } else {\n return 0;\n }\n}\n\nexport function sortParticipantsByJoinedAt(\n a: Pick<Participant, 'joinedAt'>,\n b: Pick<Participant, 'joinedAt'>,\n) {\n return (a.joinedAt?.getTime() ?? 0) - (b.joinedAt?.getTime() ?? 0);\n}\n\nexport function sortTrackReferencesByType(\n a: TrackReferenceOrPlaceholder,\n b: TrackReferenceOrPlaceholder,\n) {\n if (isTrackReference(a)) {\n if (isTrackReference(b)) {\n return 0;\n } else {\n return -1;\n }\n } else if (isTrackReference(b)) {\n return 1;\n } else {\n return 0;\n }\n}\n\n/** TrackReference with screen share source goes first. */\nexport function sortTrackReferencesByScreenShare(\n a: TrackReferenceOrPlaceholder,\n b: TrackReferenceOrPlaceholder,\n): number {\n const sourceA = getTrackReferenceSource(a);\n const sourceB = getTrackReferenceSource(b);\n\n if (sourceA === sourceB) {\n if (sourceA === Track.Source.ScreenShare) {\n if (a.participant.isLocal === b.participant.isLocal) {\n return 0;\n } else {\n return a.participant.isLocal ? 1 : -1;\n }\n }\n return 0;\n } else if (sourceA === Track.Source.ScreenShare) {\n return -1;\n } else if (sourceB === Track.Source.ScreenShare) {\n return 1;\n } else {\n return 0;\n }\n}\n\nexport function sortTrackRefsByIsCameraEnabled(\n a: { participant: { isCameraEnabled: boolean } },\n b: { participant: { isCameraEnabled: boolean } },\n) {\n const aVideo = a.participant.isCameraEnabled;\n const bVideo = b.participant.isCameraEnabled;\n\n if (aVideo !== bVideo) {\n if (aVideo) {\n return -1;\n } else {\n return 1;\n }\n } else {\n return 0;\n }\n}\n","import type { Participant } from 'livekit-client';\nimport { LocalParticipant } from 'livekit-client';\nimport {\n sortParticipantsByAudioLevel,\n sortParticipantsByIsSpeaking,\n sortParticipantsByJoinedAt,\n sortParticipantsByLastSpokenAT,\n} from './base-sort-functions';\n\n/**\n * Default sort for participants, it'll order participants by:\n * 1. local participant\n * 2. dominant speaker (speaker with the loudest audio level)\n * 3. other speakers that are recently active\n * 4. participants with video on\n * 5. by joinedAt\n */\nexport function sortParticipants(participants: Participant[]): Participant[] {\n const sortedParticipants = [...participants];\n sortedParticipants.sort((a, b) => {\n // loudest speaker first\n if (a.isSpeaking && b.isSpeaking) {\n return sortParticipantsByAudioLevel(a, b);\n }\n\n // speaker goes first\n if (a.isSpeaking !== b.isSpeaking) {\n return sortParticipantsByIsSpeaking(a, b);\n }\n\n // last active speaker first\n if (a.lastSpokeAt !== b.lastSpokeAt) {\n return sortParticipantsByLastSpokenAT(a, b);\n }\n\n // video on\n const aVideo = a.videoTrackPublications.size > 0;\n const bVideo = b.videoTrackPublications.size > 0;\n if (aVideo !== bVideo) {\n if (aVideo) {\n return -1;\n } else {\n return 1;\n }\n }\n\n // joinedAt\n return sortParticipantsByJoinedAt(a, b);\n });\n const localParticipant = sortedParticipants.find((p) => p.isLocal) as LocalParticipant;\n if (localParticipant) {\n const localIdx = sortedParticipants.indexOf(localParticipant);\n if (localIdx >= 0) {\n sortedParticipants.splice(localIdx, 1);\n if (sortedParticipants.length > 0) {\n sortedParticipants.splice(0, 0, localParticipant);\n } else {\n sortedParticipants.push(localParticipant);\n }\n }\n }\n return sortedParticipants;\n}\n","export function chunk<T>(input: Array<T>, size: number) {\n return input.reduce(\n (arr, item, idx) => {\n return idx % size === 0\n ? [...arr, [item]]\n : [...arr.slice(0, -1), [...arr.slice(-1)[0], item]];\n },\n [] as Array<Array<T>>,\n );\n}\n\nexport function zip<T, U>(a1: Array<T>, a2: Array<U>) {\n const resultLength = Math.max(a1.length, a2.length);\n return new Array(resultLength).fill([]).map((_val, idx) => [a1[idx], a2[idx]]);\n}\n\nexport function differenceBy<T>(a1: Array<T>, a2: Array<T>, by: (arg: T) => string) {\n return a1.filter((c) => !a2.map((v) => by(v)).includes(by(c)));\n}\n","/**\n * Internal test function.\n *\n * @internal\n */\n\nimport { Participant, RemoteTrackPublication, Track, TrackPublication } from 'livekit-client';\nimport type { UpdatableItem } from '../sorting/tile-array-update';\nimport type { TrackReference, TrackReferencePlaceholder } from './track-reference.types';\nimport { getTrackReferenceId } from './track-reference.utils';\nimport { TrackInfo } from '@livekit/protocol';\n\n// Test function:\nexport const mockTrackReferencePlaceholder = (\n id: string,\n source: Track.Source,\n): TrackReferencePlaceholder => {\n return { participant: new Participant(`${id}`, `${id}`), source };\n};\n\nexport const mockTrackReferencePublished = (id: string, source: Track.Source): TrackReference => {\n const kind = [Track.Source.Camera, Track.Source.ScreenShare].includes(source)\n ? Track.Kind.Video\n : Track.Kind.Audio;\n const trackInfo = new TrackInfo({\n sid: `${id}`,\n name: `${id}`,\n muted: false,\n });\n return {\n participant: new Participant(`${id}`, `${id}`),\n publication: new RemoteTrackPublication(kind, trackInfo, true),\n source: source,\n };\n};\n\ntype mockTrackReferenceSubscribedOptions = {\n mockPublication?: boolean;\n mockParticipant?: boolean;\n mockIsLocal?: boolean;\n};\n\nexport const mockTrackReferenceSubscribed = (\n id: string,\n source: Track.Source,\n options: mockTrackReferenceSubscribedOptions = {},\n): TrackReference => {\n const kind = [Track.Source.Camera, Track.Source.ScreenShare].includes(source)\n ? Track.Kind.Video\n : Track.Kind.Audio;\n const trackInfo = new TrackInfo({\n sid: `${id}`,\n name: `${id}`,\n muted: false,\n });\n const publication = new RemoteTrackPublication(kind, trackInfo, true);\n // @ts-expect-error\n publication.track = {};\n return {\n participant: options.mockParticipant\n ? (mockParticipant(id, options.mockIsLocal ?? false) as Participant)\n : new Participant(`${id}`, `${id}`),\n publication: options.mockPublication\n ? (mockTrackPublication(`publicationId(${id})`, kind, source) as TrackPublication)\n : publication,\n source,\n };\n};\n\nconst mockTrackPublication = (\n id: string,\n kind: Track.Kind,\n source: Track.Source,\n): Pick<TrackPublication, 'kind' | 'trackSid' | 'trackName' | 'source'> => {\n return {\n kind,\n trackSid: id,\n trackName: `name_${id}`,\n source: source,\n };\n};\n\nfunction mockParticipant(\n id: string,\n isLocal: boolean,\n): Pick<Participant, 'sid' | 'identity' | 'isLocal'> {\n return {\n sid: `${id}_sid`,\n identity: `${id}`,\n isLocal: isLocal,\n };\n}\n\nexport function flatTrackReferenceArray<T extends UpdatableItem>(list: T[]): string[] {\n return list.map((item) => {\n if (typeof item === 'string' || typeof item === 'number') {\n return `${item}`;\n } else {\n return getTrackReferenceId(item);\n }\n });\n}\n","import { differenceBy, chunk, zip } from '../helper/array-helper';\nimport { log } from '../logger';\nimport type { TrackReferenceOrPlaceholder } from '../track-reference';\nimport {\n getTrackReferenceId,\n isPlaceholderReplacement,\n isTrackReference,\n isTrackReferencePlaceholder,\n} from '../track-reference';\nimport { flatTrackReferenceArray } from '../track-reference/test-utils';\n\ntype VisualChanges<T> = {\n dropped: T[];\n added: T[];\n};\n\nexport type UpdatableItem = TrackReferenceOrPlaceholder | number;\n\n/** Check to see if anything visually changes on the page. */\nexport function visualPageChange<T extends UpdatableItem>(state: T[], next: T[]): VisualChanges<T> {\n return {\n dropped: differenceBy(state, next, getTrackReferenceId),\n added: differenceBy(next, state, getTrackReferenceId),\n };\n}\n\nfunction listNeedsUpdating<T>(changes: VisualChanges<T>): boolean {\n return changes.added.length !== 0 || changes.dropped.length !== 0;\n}\n\nexport function findIndex<T extends UpdatableItem>(\n trackReference: T,\n trackReferences: T[],\n): number {\n const indexToReplace = trackReferences.findIndex(\n (trackReference_) =>\n getTrackReferenceId(trackReference_) === getTrackReferenceId(trackReference),\n );\n if (indexToReplace === -1) {\n throw new Error(\n `Element not part of the array: ${getTrackReferenceId(\n trackReference,\n )} not in ${flatTrackReferenceArray(trackReferences)}`,\n );\n }\n return indexToReplace;\n}\n\n/** Swap items in the complete list of all elements */\nexport function swapItems<T extends UpdatableItem>(\n moveForward: T,\n moveBack: T,\n trackReferences: T[],\n): T[] {\n const indexToReplace = findIndex(moveForward, trackReferences);\n const indexReplaceWith = findIndex(moveBack, trackReferences);\n\n trackReferences.splice(indexToReplace, 1, moveBack);\n trackReferences.splice(indexReplaceWith, 1, moveForward);\n\n return trackReferences;\n}\n\nexport function dropItem<T extends UpdatableItem>(itemToDrop: T, list: T[]): T[] {\n const indexOfElementToDrop = findIndex(itemToDrop, list);\n // const indexOfElementToDrop = list.findIndex((item) => item === itemToDrop, list);\n list.splice(indexOfElementToDrop, 1);\n return list;\n}\n\nfunction addItem<T extends UpdatableItem>(itemToAdd: T, list: T[]): T[] {\n return [...list, itemToAdd];\n}\n\nexport function divideIntoPages<T>(list: T[], maxElementsOnPage: number): Array<T[]> {\n const pages = chunk(list, maxElementsOnPage);\n return pages;\n}\n\n/** Divide the list of elements into pages and and check if pages need updating. */\nexport function updatePages<T extends UpdatableItem>(\n currentList: T[],\n nextList: T[],\n maxItemsOnPage: number,\n): T[] {\n let updatedList: T[] = refreshList(currentList, nextList);\n\n if (updatedList.length < nextList.length) {\n // Items got added: Find newly added items and add them to the end of the list.\n const addedItems = differenceBy(nextList, updatedList, getTrackReferenceId);\n updatedList = [...updatedList, ...addedItems];\n }\n const currentPages = divideIntoPages(updatedList, maxItemsOnPage);\n const nextPages = divideIntoPages(nextList, maxItemsOnPage);\n\n zip(currentPages, nextPages).forEach(([currentPage, nextPage], pageIndex) => {\n if (currentPage && nextPage) {\n // 1) Identify missing tile.\n const updatedPage = divideIntoPages(updatedList, maxItemsOnPage)[pageIndex];\n const changes = visualPageChange(updatedPage, nextPage);\n\n if (listNeedsUpdating(changes)) {\n log.debug(\n `Detected visual changes on page: ${pageIndex}, current: ${flatTrackReferenceArray(\n currentPage,\n )}, next: ${flatTrackReferenceArray(nextPage)}`,\n { changes },\n );\n // ## Swap Items\n if (changes.added.length === changes.dropped.length) {\n zip(changes.added, changes.dropped).forEach(([added, dropped]) => {\n if (added && dropped) {\n updatedList = swapItems<T>(added, dropped, updatedList);\n } else {\n throw new Error(\n `For a swap action we need a addition and a removal one is missing: ${added}, ${dropped}`,\n );\n }\n });\n }\n // ## Handle Drop Items\n if (changes.added.length === 0 && changes.dropped.length > 0) {\n changes.dropped.forEach((item) => {\n updatedList = dropItem<T>(item, updatedList);\n });\n }\n // ## Handle Item added\n if (changes.added.length > 0 && changes.dropped.length === 0) {\n changes.added.forEach((item) => {\n updatedList = addItem<T>(item, updatedList);\n });\n }\n }\n }\n });\n\n if (updatedList.length > nextList.length) {\n // Items got removed: Find items that got completely removed from the list.\n const missingItems = differenceBy(updatedList, nextList, getTrackReferenceId);\n updatedList = updatedList.filter(\n (item) => !missingItems.map(getTrackReferenceId).includes(getTrackReferenceId(item)),\n );\n }\n\n return updatedList;\n}\n\n/**\n * Update the current list with the items from the next list whenever the item ids are the same\n * or the current item is a placeholder and we find a track reference in the next list\n * to replace the placeholder with.\n * @remarks\n * This is needed because `TrackReference`s can change their internal state while keeping the same id.\n */\nfunction refreshList<T extends UpdatableItem>(currentList: T[], nextList: T[]): T[] {\n return currentList.map((currentItem) => {\n const updateForCurrentItem = nextList.find(\n (newItem_) =>\n // If the IDs match or ..\n getTrackReferenceId(currentItem) === getTrackReferenceId(newItem_) ||\n // ... if the current item is a placeholder and the new item is the track reference can replace it.\n (typeof currentItem !== 'number' &&\n isTrackReferencePlaceholder(currentItem) &&\n isTrackReference(newItem_) &&\n isPlaceholderReplacement(currentItem, newItem_)),\n );\n return updateForCurrentItem ?? currentItem;\n });\n}\n","import type {\n AudioCaptureOptions,\n LocalParticipant,\n Room,\n ScreenShareCaptureOptions,\n TrackPublishOptions,\n VideoCaptureOptions,\n} from 'livekit-client';\nimport { Track } from 'livekit-client';\nimport type { Observable } from 'rxjs';\nimport { Subject, map, startWit