feat(web/app): vendor the LiveKit component set
Keep the third-party components, styles and licences byte-identical so an upstream tarball can be replayed; it is excluded from formatting and lint.
ZhenYi committed
Added view/app/src/vendor/livekit/core/components/chat.ts view/app/src/vendor/livekit/core/components/chat.ts +309 −0 View view/app/src/vendor/livekit/core/components/chat.ts
@@ -0,0 +1,309 @@
1/* eslint-disable camelcase */
2import type { Room, SendTextOptions } from 'livekit-client';
3import { compareVersions, RoomEvent } from 'livekit-client';
4import {
5 BehaviorSubject,
6 Subject,
7 scan,
8 map,
9 takeUntil,
10 from,
11 filter,
12 concatMap,
13 mergeMap,
14 finalize,
15 of,
16} from 'rxjs';
17import {
18 DataTopic,
19 LegacyDataTopic,
20 sendMessage,
21 setupDataMessageHandler,
22} from '../observables/dataChannel';
23import { log } from '../logger';
24import { type ChatMessage, type ReceivedChatMessage } from '../messages/types';
25import { Future } from '../helper/future';
26
27/** @public */
28export type { ChatMessage, ReceivedChatMessage };
29
30export interface LegacyChatMessage extends ChatMessage {
31 ignoreLegacy?: boolean;
32}
33
34export interface LegacyReceivedChatMessage extends ReceivedChatMessage {
35 ignoreLegacy?: boolean;
36}
37
38/**
39 * @public
40 * @deprecated the new chat API doesn't rely on encoders and decoders anymore and uses a dedicated chat API instead
41 */
42export type MessageEncoder = (message: LegacyChatMessage) => Uint8Array;
43/**
44 * @public
45 * @deprecated the new chat API doesn't rely on encoders and decoders anymore and uses a dedicated chat API instead
46 */
47export type MessageDecoder = (message: Uint8Array) => LegacyReceivedChatMessage;
48/** @public */
49export type ChatOptions = {
50 /** @deprecated the new chat API doesn't rely on encoders and decoders anymore and uses a dedicated chat API instead */
51 messageEncoder?: (message: LegacyChatMessage) => Uint8Array;
52 /** @deprecated the new chat API doesn't rely on encoders and decoders anymore and uses a dedicated chat API instead */
53 messageDecoder?: (message: Uint8Array) => LegacyReceivedChatMessage;
54 channelTopic?: string;
55 /** @deprecated the new chat API doesn't rely on update topics anymore and uses a dedicated chat API instead */
56 updateChannelTopic?: string;
57};
58
59const topicSubjectMap: WeakMap<Room, Map<string, Subject<ReceivedChatMessage>>> = new WeakMap();
60const streamIdToAttachments = new Map<
61 string /* stream id */,
62 Map<
63 string /* attachment id */,
64 Future<
65 {
66 fileName: string;
67 mimeType: string;
68 buffer: Array<Uint8Array>;
69 },
70 never
71 >
72 >
73>();
74
75function isIgnorableChatMessage(msg: ReceivedChatMessage | LegacyReceivedChatMessage) {
76 return (msg as LegacyChatMessage).ignoreLegacy == true;
77}
78
79const decodeLegacyMsg = (message: Uint8Array) =>
80 JSON.parse(new TextDecoder().decode(message)) as
81 LegacyReceivedChatMessage | Exclude<ReceivedChatMessage, 'type'>;
82
83const encodeLegacyMsg = (message: LegacyChatMessage) =>
84 new TextEncoder().encode(JSON.stringify(message));
85
86export function setupChat(room: Room, options?: ChatOptions) {
87 const serverSupportsDataStreams = () =>
88 room.serverInfo?.edition === 1 ||
89 (!!room.serverInfo?.version && compareVersions(room.serverInfo?.version, '1.8.2') > 0);
90
91 const onDestroyObservable = new Subject<void>();
92
93 const topic = options?.channelTopic ?? DataTopic.CHAT;
94 const legacyTopic = options?.channelTopic ?? LegacyDataTopic.CHAT;
95
96 let needsSetup = false;
97 if (!topicSubjectMap.has(room)) {
98 needsSetup = true;
99 }
100 const topicMap = topicSubjectMap.get(room) ?? new Map<string, Subject<ReceivedChatMessage>>();
101 const messageSubject = topicMap.get(topic) ?? new Subject<ReceivedChatMessage>();
102 topicMap.set(topic, messageSubject);
103 topicSubjectMap.set(room, topicMap);
104
105 const finalMessageDecoder = options?.messageDecoder ?? decodeLegacyMsg;
106 if (needsSetup) {
107 room.registerTextStreamHandler(topic, async (reader, participantInfo) => {
108 const { id, timestamp, attributes, attachedStreamIds } = reader.info;
109
110 // Store a future for each attachment to be later resolved once the corresponding file data
111 // stream completes.
112 const attachments = new Map(
113 (attachedStreamIds ?? []).map((id) => [
114 id,
115 new Future<{ fileName: string; mimeType: string; buffer: Array<Uint8Array> }, never>(),
116 ]),
117 );
118 streamIdToAttachments.set(id, attachments);
119
120 const streamObservable = from(reader).pipe(
121 scan((acc: string, chunk: string) => {
122 return acc + chunk;
123 }),
124 mergeMap((chunk: string) => {
125 if (attachments.size === 0) {
126 return of({ chunk, attachedFiles: [] });
127 } else {
128 // Aggregate all attachments into memory and transform them into a list of files
129 return from(attachments.values()).pipe(
130 // `concatMap`, not `mergeMap`: `attachments` is built from `attachedStreamIds`, so it is
131 // already in the sender's order, but `mergeMap` emits each promise as it RESOLVES — which
132 // ordered `attachedFiles` by download completion (i.e. smallest file first). All the
133 // promises are in flight either way; `concatMap` only sequences the emissions.
134 concatMap((attachment) => from(attachment.promise)),
135 scan(
136 (acc, attachment) => [
137 ...acc,
138 // Preserve the MIME type: `new File(buffer, name)` leaves `File.type` empty, and
139 // consumers (including `ChatEntry`) test `file.type.startsWith('image/')`, which could
140 // never be true for a received attachment.
141 new File(attachment.buffer as BlobPart[], attachment.fileName, { type: attachment.mimeType }),
142 ],
143 [] as Array<File>,
144 ),
145 map((attachedFiles) => ({ chunk, attachedFiles })),
146 );
147 }
148 }),
149 map(({ chunk, attachedFiles }) => {
150 return {
151 id,
152 timestamp,
153 message: chunk,
154 from: room.getParticipantByIdentity(participantInfo.identity),
155 type: 'chatMessage',
156 attributes,
157 attachedFiles,
158 // editTimestamp: type === 'update' ? timestamp : undefined,
159 } satisfies ReceivedChatMessage;
160 }),
161 finalize(() => streamIdToAttachments.delete(id)),
162 );
163 streamObservable.subscribe({
164 next: (value) => messageSubject.next(value),
165 });
166 });
167 // NOTE: Attachment byte streams are guaranteed to arrive after their parent text stream
168 // has initialized the attachment map (per client SDK sending implementation)
169 room.registerByteStreamHandler(topic, async (reader) => {
170 const { id: attachmentStreamId } = reader.info;
171 const foundStreamAttachmentPair = Array.from(streamIdToAttachments).find(([, attachments]) =>
172 attachments.has(attachmentStreamId),
173 );
174 if (!foundStreamAttachmentPair) {
175 return;
176 }
177 const streamId = foundStreamAttachmentPair[0];
178
179 const bufferList = [];
180 for await (const buffer of reader) {
181 bufferList.push(buffer);
182 }
183
184 const attachment = streamIdToAttachments.get(streamId)?.get(attachmentStreamId);
185 if (!attachment) {
186 return;
187 }
188
189 attachment.resolve?.({
190 fileName: reader.info.name,
191 mimeType: reader.info.mimeType,
192 buffer: bufferList,
193 });
194 });
195
196 /** legacy chat protocol handling */
197 const { messageObservable } = setupDataMessageHandler(room, [legacyTopic]);
198 messageObservable
199 .pipe(
200 map((msg) => {
201 const parsedMessage = finalMessageDecoder(msg.payload);
202 if (isIgnorableChatMessage(parsedMessage)) {
203 return undefined;
204 }
205 const newMessage: ReceivedChatMessage = {
206 ...parsedMessage,
207 type: 'chatMessage',
208 from: msg.from,
209 };
210 return newMessage;
211 }),
212 filter((msg) => !!msg),
213 takeUntil(onDestroyObservable),
214 )
215 .subscribe(messageSubject);
216 }
217
218 /** Build up the message array over time. */
219 const messagesObservable = messageSubject.pipe(
220 scan<ReceivedChatMessage, ReceivedChatMessage[]>((acc, value) => {
221 if (
222 'id' in value &&
223 acc.find((msg) => msg.from?.identity === value.from?.identity && msg.id === value.id)
224 ) {
225 const replaceIndex = acc.findIndex((msg) => msg.id === value.id);
226 if (replaceIndex > -1) {
227 const originalMsg = acc[replaceIndex];
228 acc[replaceIndex] = {
229 ...value,
230 timestamp: originalMsg.timestamp,
231 editTimestamp: value.timestamp,
232 };
233 }
234 return [...acc];
235 }
236 return [...acc, value];
237 }, []),
238 takeUntil(onDestroyObservable),
239 );
240
241 const isSending$ = new BehaviorSubject<boolean>(false);
242 const finalMessageEncoder = options?.messageEncoder ?? encodeLegacyMsg;
243
244 const send = async (message: string, options?: SendTextOptions) => {
245 if (!options) {
246 options = {};
247 }
248 options.topic ??= topic;
249 isSending$.next(true);
250
251 try {
252 const info = await room.localParticipant.sendText(message, options);
253
254 const legacyChatMsg: LegacyChatMessage = {
255 id: info.id,
256 timestamp: Date.now(),
257 message,
258 };
259
260 const chatMsg: ChatMessage = {
261 ...legacyChatMsg,
262 attachedFiles: options.attachments,
263 };
264
265 const receivedChatMsg: ReceivedChatMessage = {
266 ...chatMsg,
267 type: 'chatMessage',
268 from: room.localParticipant,
269 attributes: options.attributes,
270 };
271
272 messageSubject.next(receivedChatMsg);
273
274 const encodedLegacyMsg = finalMessageEncoder({
275 ...legacyChatMsg,
276 ignoreLegacy: serverSupportsDataStreams(),
277 });
278
279 try {
280 await sendMessage(room.localParticipant, encodedLegacyMsg, {
281 reliable: true,
282 topic: legacyTopic,
283 });
284 } catch (error) {
285 log.info('could not send message in legacy chat format', error);
286 }
287
288 return receivedChatMsg;
289 } finally {
290 isSending$.next(false);
291 }
292 };
293
294 function destroy() {
295 onDestroyObservable.next();
296 onDestroyObservable.complete();
297 messageSubject.complete();
298 topicSubjectMap.delete(room);
299 room.unregisterTextStreamHandler(topic);
300 room.unregisterByteStreamHandler(topic);
301 }
302 room.once(RoomEvent.Disconnected, destroy);
303
304 return {
305 messageObservable: messagesObservable,
306 isSendingObservable: isSending$,
307 send,
308 };
309}
Added view/app/src/vendor/livekit/core/components/chatToggle.ts view/app/src/vendor/livekit/core/components/chatToggle.ts +6 −0 View view/app/src/vendor/livekit/core/components/chatToggle.ts
@@ -0,0 +1,6 @@
1import { prefixClass } from '../styles-interface';
2
3export function setupChatToggle() {
4 const className: string = [prefixClass('button'), prefixClass('chat-toggle')].join(' ');
5 return { className };
6}
Added view/app/src/vendor/livekit/core/components/clearPinButton.ts view/app/src/vendor/livekit/core/components/clearPinButton.ts +7 −0 View view/app/src/vendor/livekit/core/components/clearPinButton.ts
@@ -0,0 +1,7 @@
1// import { prefixClass } from '../styles-interface';
2
3export function setupClearPinButton() {
4 // const className = prefixClass('clear-pin-button');
5 const className = 'lk-clear-pin-button lk-button';
6 return { className };
7}
Added view/app/src/vendor/livekit/core/components/connectionQualityIndicator.ts view/app/src/vendor/livekit/core/components/connectionQualityIndicator.ts +9 −0 View view/app/src/vendor/livekit/core/components/connectionQualityIndicator.ts
@@ -0,0 +1,9 @@
1import type { Participant } from 'livekit-client';
2import { createConnectionQualityObserver } from '../observables/participant';
3import { prefixClass } from '../styles-interface';
4
5export function setupConnectionQualityIndicator(participant: Participant) {
6 const className = prefixClass('connection-quality');
7 const connectionQualityObserver = createConnectionQualityObserver(participant);
8 return { className, connectionQualityObserver };
9}
Added view/app/src/vendor/livekit/core/components/disconnectButton.ts view/app/src/vendor/livekit/core/components/disconnectButton.ts +10 −0 View view/app/src/vendor/livekit/core/components/disconnectButton.ts
@@ -0,0 +1,10 @@
1import type { Room } from 'livekit-client';
2import { prefixClass } from '../styles-interface';
3
4export function setupDisconnectButton(room: Room) {
5 const disconnect = (stopTracks?: boolean) => {
6 room.disconnect(stopTracks);
7 };
8 const className: string = prefixClass('disconnect-button');
9 return { className, disconnect };
10}
Added view/app/src/vendor/livekit/core/components/focusToggle.ts view/app/src/vendor/livekit/core/components/focusToggle.ts +6 −0 View view/app/src/vendor/livekit/core/components/focusToggle.ts
@@ -0,0 +1,6 @@
1import { prefixClass } from '../styles-interface';
2
3export function setupFocusToggle() {
4 const className: string = [prefixClass('button'), prefixClass('focus-toggle-button')].join(' ');
5 return { className };
6}
Added view/app/src/vendor/livekit/core/components/mediaDeviceSelect.ts view/app/src/vendor/livekit/core/components/mediaDeviceSelect.ts +70 −0 View view/app/src/vendor/livekit/core/components/mediaDeviceSelect.ts
@@ -0,0 +1,70 @@
1import {
2 Track,
3 type LocalAudioTrack,
4 type LocalVideoTrack,
5 type Room,
6 type LocalTrack,
7 getBrowser,
8} from 'livekit-client';
9import { BehaviorSubject } from 'rxjs';
10import { log } from '../logger';
11import { prefixClass } from '../styles-interface';
12import { createActiveDeviceObservable } from '../observables/room';
13
14export type SetMediaDeviceOptions = {
15 /**
16 * If true, adds an `exact` constraint to the getUserMedia request.
17 * The request will fail if this option is true and the device specified is not actually available
18 */
19 exact?: boolean;
20};
21
22export function setupDeviceSelector(
23 kind: MediaDeviceKind,
24 room: Room,
25 localTrack?: LocalAudioTrack | LocalVideoTrack,
26) {
27 const activeDeviceSubject = new BehaviorSubject<string | undefined>(undefined);
28
29 const activeDeviceObservable = createActiveDeviceObservable(room, kind);
30
31 const setActiveMediaDevice = async (id: string, options: SetMediaDeviceOptions = {}) => {
32 if (localTrack) {
33 await localTrack.setDeviceId(options.exact ? { exact: id } : id);
34 const actualId = await localTrack.getDeviceId(false);
35 activeDeviceSubject.next(
36 id === 'default' && localTrack.mediaStreamTrack.label.startsWith('Default') ? id : actualId,
37 );
38 } else if (room) {
39 const browser = getBrowser();
40 if (kind === 'audiooutput' && (browser?.name === 'Safari' || browser?.os === 'iOS')) {
41 log.warn(`Switching audio output device is not supported on Safari and iOS.`);
42 return;
43 }
44 log.debug(`Switching active device of kind "${kind}" with id ${id}.`);
45 await room.switchActiveDevice(kind, id, options.exact);
46 const actualDeviceId: string | undefined = room.getActiveDevice(kind) ?? id;
47 if (actualDeviceId !== id && id !== 'default') {
48 log.info(
49 `We tried to select the device with id (${id}), but the browser decided to select the device with id (${actualDeviceId}) instead.`,
50 );
51 }
52 let targetTrack: LocalTrack | undefined = undefined;
53 if (kind === 'audioinput')
54 targetTrack = room.localParticipant.getTrackPublication(Track.Source.Microphone)?.track;
55 else if (kind === 'videoinput') {
56 targetTrack = room.localParticipant.getTrackPublication(Track.Source.Camera)?.track;
57 }
58 const useDefault =
59 (id === 'default' && !targetTrack) ||
60 (id === 'default' && targetTrack?.mediaStreamTrack.label.startsWith('Default'));
61 activeDeviceSubject.next(useDefault ? id : actualDeviceId);
62 }
63 };
64 const className: string = prefixClass('media-device-select');
65 return {
66 className,
67 activeDeviceObservable,
68 setActiveMediaDevice,
69 };
70}
Added view/app/src/vendor/livekit/core/components/mediaToggle.ts view/app/src/vendor/livekit/core/components/mediaToggle.ts +145 −0 View view/app/src/vendor/livekit/core/components/mediaToggle.ts
@@ -0,0 +1,145 @@
1import type {
2 AudioCaptureOptions,
3 LocalParticipant,
4 Room,
5 ScreenShareCaptureOptions,
6 TrackPublishOptions,
7 VideoCaptureOptions,
8} from 'livekit-client';
9import { Track } from 'livekit-client';
10import type { Observable } from 'rxjs';
11import { Subject, map, startWith } from 'rxjs';
12import { observeParticipantMedia } from '../observables/participant';
13import { prefixClass } from '../styles-interface';
14
15export type CaptureOptionsBySource<T extends ToggleSource> = T extends Track.Source.Camera
16 ? VideoCaptureOptions
17 : T extends Track.Source.Microphone
18 ? AudioCaptureOptions
19 : T extends Track.Source.ScreenShare
20 ? ScreenShareCaptureOptions
21 : never;
22
23export type MediaToggleType<T extends ToggleSource> = {
24 pendingObserver: Observable<boolean>;
25 toggle: (
26 forceState?: boolean,
27 captureOptions?: CaptureOptionsBySource<T>,
28 ) => Promise<boolean | undefined>;
29 className: string;
30 enabledObserver: Observable<boolean>;
31};
32
33export type ToggleSource = Exclude<
34 Track.Source,
35 Track.Source.ScreenShareAudio | Track.Source.Unknown
36>;
37
38export function setupMediaToggle<T extends ToggleSource>(
39 source: T,
40 room: Room,
41 options?: CaptureOptionsBySource<T>,
42 publishOptions?: TrackPublishOptions,
43 onError?: (error: Error) => void,
44): MediaToggleType<T> {
45 const { localParticipant } = room;
46
47 const getSourceEnabled = (source: ToggleSource, localParticipant: LocalParticipant) => {
48 let isEnabled = false;
49 switch (source) {
50 case Track.Source.Camera:
51 isEnabled = localParticipant.isCameraEnabled;
52 break;
53 case Track.Source.Microphone:
54 isEnabled = localParticipant.isMicrophoneEnabled;
55 break;
56 case Track.Source.ScreenShare:
57 isEnabled = localParticipant.isScreenShareEnabled;
58 break;
59 default:
60 break;
61 }
62 return isEnabled;
63 };
64
65 const enabledObserver = observeParticipantMedia(localParticipant).pipe(
66 map((media) => {
67 return getSourceEnabled(source, media.participant as LocalParticipant);
68 }),
69 startWith(getSourceEnabled(source, localParticipant)),
70 );
71
72 const pendingSubject = new Subject<boolean>();
73 const toggle = async (forceState?: boolean, captureOptions?: CaptureOptionsBySource<T>) => {
74 try {
75 captureOptions ??= options;
76 // trigger observable update
77 pendingSubject.next(true);
78 switch (source) {
79 case Track.Source.Camera:
80 await localParticipant.setCameraEnabled(
81 forceState ?? !localParticipant.isCameraEnabled,
82 captureOptions as VideoCaptureOptions,
83 publishOptions,
84 );
85 return localParticipant.isCameraEnabled;
86 case Track.Source.Microphone:
87 await localParticipant.setMicrophoneEnabled(
88 forceState ?? !localParticipant.isMicrophoneEnabled,
89 captureOptions as AudioCaptureOptions,
90 publishOptions,
91 );
92 return localParticipant.isMicrophoneEnabled;
93 case Track.Source.ScreenShare:
94 await localParticipant.setScreenShareEnabled(
95 forceState ?? !localParticipant.isScreenShareEnabled,
96 captureOptions as ScreenShareCaptureOptions,
97 publishOptions,
98 );
99 return localParticipant.isScreenShareEnabled;
100 default:
101 throw new TypeError('Tried to toggle unsupported source');
102 }
103 } catch (e) {
104 if (onError && e instanceof Error) {
105 onError?.(e);
106 return undefined;
107 } else {
108 throw e;
109 }
110 } finally {
111 pendingSubject.next(false);
112 // trigger observable update
113 }
114 };
115
116 const className: string = prefixClass('button');
117 return {
118 className,
119 toggle,
120 enabledObserver,
121 pendingObserver: pendingSubject.asObservable(),
122 };
123}
124
125export function setupManualToggle() {
126 let state = false;
127
128 const enabledSubject = new Subject<boolean>();
129
130 const pendingSubject = new Subject<boolean>();
131
132 const toggle = async (forceState?: boolean) => {
133 pendingSubject.next(true);
134 state = forceState ?? !state;
135 enabledSubject.next(state);
136 pendingSubject.next(false);
137 };
138 const className: string = prefixClass('button');
139 return {
140 className,
141 toggle,
142 enabledObserver: enabledSubject.asObservable(),
143 pendingObserver: pendingSubject.asObservable(),
144 };
145}
Added view/app/src/vendor/livekit/core/components/mediaTrack.ts view/app/src/vendor/livekit/core/components/mediaTrack.ts +42 −0 View view/app/src/vendor/livekit/core/components/mediaTrack.ts
@@ -0,0 +1,42 @@
1import { Track } from 'livekit-client';
2import { map, startWith } from 'rxjs';
3import { observeParticipantMedia } from '../observables/participant';
4import { prefixClass } from '../styles-interface';
5import { isTrackReference } from '../track-reference/track-reference.types';
6import type { TrackIdentifier } from '../types';
7
8export function setupMediaTrack(trackIdentifier: TrackIdentifier) {
9 const initialPub = getTrackByIdentifier(trackIdentifier);
10 const trackObserver = observeParticipantMedia(trackIdentifier.participant).pipe(
11 map(() => {
12 return getTrackByIdentifier(trackIdentifier);
13 }),
14 startWith(initialPub),
15 );
16 const className: string = prefixClass(
17 trackIdentifier.source === Track.Source.Camera ||
18 trackIdentifier.source === Track.Source.ScreenShare
19 ? 'participant-media-video'
20 : 'participant-media-audio',
21 );
22 return { className, trackObserver };
23}
24
25export function getTrackByIdentifier(options: TrackIdentifier) {
26 if (isTrackReference(options)) {
27 return options.publication;
28 } else {
29 const { source, name, participant } = options;
30 if (source && name) {
31 return participant
32 .getTrackPublications()
33 .find((pub) => pub.source === source && pub.trackName === name);
34 } else if (name) {
35 return participant.getTrackPublicationByName(name);
36 } else if (source) {
37 return participant.getTrackPublication(source);
38 } else {
39 throw new Error('At least one of source and name needs to be defined');
40 }
41 }
42}
Added view/app/src/vendor/livekit/core/components/participantName.ts view/app/src/vendor/livekit/core/components/participantName.ts +7 −0 View view/app/src/vendor/livekit/core/components/participantName.ts
@@ -0,0 +1,7 @@
1import type { Participant } from 'livekit-client';
2import { participantInfoObserver } from '../observables/participant';
3
4export function setupParticipantName(participant: Participant) {
5 const infoObserver = participantInfoObserver(participant);
6 return { className: 'lk-participant-name', infoObserver };
7}
Added view/app/src/vendor/livekit/core/components/participantTile.ts view/app/src/vendor/livekit/core/components/participantTile.ts +8 −0 View view/app/src/vendor/livekit/core/components/participantTile.ts
@@ -0,0 +1,8 @@
1import { prefixClass } from '../styles-interface';
2
3export function setupParticipantTile() {
4 const className: string = prefixClass('participant-tile');
5 return {
6 className,
7 };
8}
Added view/app/src/vendor/livekit/core/components/room.ts view/app/src/vendor/livekit/core/components/room.ts +4 −0 View view/app/src/vendor/livekit/core/components/room.ts
@@ -0,0 +1,4 @@
1export function setupLiveKitRoom() {
2 const className = 'lk-room-container';
3 return { className };
4}
Added view/app/src/vendor/livekit/core/components/startAudio.ts view/app/src/vendor/livekit/core/components/startAudio.ts +13 −0 View view/app/src/vendor/livekit/core/components/startAudio.ts
@@ -0,0 +1,13 @@
1import type { Room } from 'livekit-client';
2import { log } from '../logger';
3import { roomAudioPlaybackAllowedObservable } from '../observables/room';
4import { prefixClass } from '../styles-interface';
5
6export function setupStartAudio() {
7 const handleStartAudioPlayback = async (room: Room) => {
8 log.info('Start Audio for room: ', room);
9 await room.startAudio();
10 };
11 const className: string = prefixClass('start-audio-button');
12 return { className, roomAudioPlaybackAllowedObservable, handleStartAudioPlayback };
13}
Added view/app/src/vendor/livekit/core/components/startVideo.ts view/app/src/vendor/livekit/core/components/startVideo.ts +13 −0 View view/app/src/vendor/livekit/core/components/startVideo.ts
@@ -0,0 +1,13 @@
1import type { Room } from 'livekit-client';
2import { log } from '../logger';
3import { roomVideoPlaybackAllowedObservable } from '../observables/room';
4import { prefixClass } from '../styles-interface';
5
6export function setupStartVideo() {
7 const handleStartVideoPlayback = async (room: Room) => {
8 log.info('Start Video for room: ', room);
9 await room.startVideo();
10 };
11 const className: string = prefixClass('start-audio-button');
12 return { className, roomVideoPlaybackAllowedObservable, handleStartVideoPlayback };
13}
Added view/app/src/vendor/livekit/core/components/trackMutedIndicator.ts view/app/src/vendor/livekit/core/components/trackMutedIndicator.ts +25 −0 View view/app/src/vendor/livekit/core/components/trackMutedIndicator.ts
@@ -0,0 +1,25 @@
1// @ts-ignore some module resolutions (other than 'node') choke on this
2import type { Styles } from '@livekit/components-styles/dist/types_unprefixed/index.scss';
3import { Track } from 'livekit-client';
4import { mutedObserver } from '../observables/participant';
5import { prefixClass } from '../styles-interface';
6import type { TrackReferenceOrPlaceholder } from '../track-reference';
7
8export function setupTrackMutedIndicator(trackRef: TrackReferenceOrPlaceholder) {
9 let classForSource: keyof Styles = 'track-muted-indicator-camera';
10 switch (trackRef.source) {
11 case Track.Source.Camera:
12 classForSource = 'track-muted-indicator-camera';
13 break;
14 case Track.Source.Microphone:
15 classForSource = 'track-muted-indicator-microphone';
16 break;
17
18 default:
19 break;
20 }
21 const className: string = prefixClass(classForSource);
22 const mediaMutedObserver = mutedObserver(trackRef);
23
24 return { className, mediaMutedObserver };
25}
Added view/app/src/vendor/livekit/core/helper/array-helper.ts view/app/src/vendor/livekit/core/helper/array-helper.ts +19 −0 View view/app/src/vendor/livekit/core/helper/array-helper.ts
@@ -0,0 +1,19 @@
1export function chunk<T>(input: Array<T>, size: number) {
2 return input.reduce(
3 (arr, item, idx) => {
4 return idx % size === 0
5 ? [...arr, [item]]
6 : [...arr.slice(0, -1), [...arr.slice(-1)[0], item]];
7 },
8 [] as Array<Array<T>>,
9 );
10}
11
12export function zip<T, U>(a1: Array<T>, a2: Array<U>) {
13 const resultLength = Math.max(a1.length, a2.length);
14 return new Array(resultLength).fill([]).map((_val, idx) => [a1[idx], a2[idx]]);
15}
16
17export function differenceBy<T>(a1: Array<T>, a2: Array<T>, by: (arg: T) => string) {
18 return a1.filter((c) => !a2.map((v) => by(v)).includes(by(c)));
19}
Added view/app/src/vendor/livekit/core/helper/detectMobileBrowser.ts view/app/src/vendor/livekit/core/helper/detectMobileBrowser.ts +19 −0 View view/app/src/vendor/livekit/core/helper/detectMobileBrowser.ts
@@ -0,0 +1,19 @@
1/**
2 * @internal
3 */
4export function isWeb(): boolean {
5 return typeof document !== 'undefined';
6}
7
8/**
9 * Mobile browser detection based on `navigator.userAgent` string.
10 * Defaults to returning `false` if not in a browser.
11 *
12 * @remarks
13 * This should only be used if feature detection or other methods do not work!
14 *
15 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#mobile_device_detection
16 */
17export function isMobileBrowser(): boolean {
18 return isWeb() ? /Mobi/i.test(window.navigator.userAgent) : false;
19}
Added view/app/src/vendor/livekit/core/helper/emailRegex.ts view/app/src/vendor/livekit/core/helper/emailRegex.ts +8 −0 View view/app/src/vendor/livekit/core/helper/emailRegex.ts
@@ -0,0 +1,8 @@
1// source code adapted from https://github.com/sindresorhus/email-regex due to ESM import incompatibilities when trying to serve a CJS version of components
2
3const regex = '[^\\.\\s@:](?:[^\\s@:]*[^\\s@:\\.])?@[^\\.\\s@]+(?:\\.[^\\.\\s@]+)*';
4
5function createEmailRegExp({ exact }: { exact?: boolean } = {}) {
6 return exact ? new RegExp(`^${regex}$`) : new RegExp(regex, 'g');
7}
8export { createEmailRegExp };
Added view/app/src/vendor/livekit/core/helper/eventGroups.ts view/app/src/vendor/livekit/core/helper/eventGroups.ts +67 −0 View view/app/src/vendor/livekit/core/helper/eventGroups.ts
@@ -0,0 +1,67 @@
1import { ParticipantEvent, RoomEvent } from 'livekit-client';
2
3export const allRemoteParticipantRoomEvents = [
4 RoomEvent.ConnectionStateChanged,
5 RoomEvent.RoomMetadataChanged,
6
7 RoomEvent.ActiveSpeakersChanged,
8 RoomEvent.ConnectionQualityChanged,
9
10 RoomEvent.ParticipantConnected,
11 RoomEvent.ParticipantDisconnected,
12 RoomEvent.ParticipantPermissionsChanged,
13 RoomEvent.ParticipantMetadataChanged,
14 RoomEvent.ParticipantNameChanged,
15 RoomEvent.ParticipantAttributesChanged,
16
17 RoomEvent.TrackMuted,
18 RoomEvent.TrackUnmuted,
19 RoomEvent.TrackPublished,
20 RoomEvent.TrackUnpublished,
21 RoomEvent.TrackStreamStateChanged,
22 RoomEvent.TrackSubscriptionFailed,
23 RoomEvent.TrackSubscriptionPermissionChanged,
24 RoomEvent.TrackSubscriptionStatusChanged,
25];
26
27export const allParticipantRoomEvents = [
28 ...allRemoteParticipantRoomEvents,
29 RoomEvent.LocalTrackPublished,
30 RoomEvent.LocalTrackUnpublished,
31];
32
33export const participantTrackEvents = [
34 ParticipantEvent.TrackPublished,
35 ParticipantEvent.TrackUnpublished,
36 ParticipantEvent.TrackMuted,
37 ParticipantEvent.TrackUnmuted,
38 ParticipantEvent.TrackStreamStateChanged,
39 ParticipantEvent.TrackSubscribed,
40 ParticipantEvent.TrackUnsubscribed,
41 ParticipantEvent.TrackSubscriptionPermissionChanged,
42 ParticipantEvent.TrackSubscriptionFailed,
43 ParticipantEvent.LocalTrackPublished,
44 ParticipantEvent.LocalTrackUnpublished,
45];
46
47export const allRemoteParticipantEvents = [
48 ParticipantEvent.ConnectionQualityChanged,
49 ParticipantEvent.IsSpeakingChanged,
50 ParticipantEvent.ParticipantMetadataChanged,
51 ParticipantEvent.ParticipantPermissionsChanged,
52
53 ParticipantEvent.TrackMuted,
54 ParticipantEvent.TrackUnmuted,
55 ParticipantEvent.TrackPublished,
56 ParticipantEvent.TrackUnpublished,
57 ParticipantEvent.TrackStreamStateChanged,
58 ParticipantEvent.TrackSubscriptionFailed,
59 ParticipantEvent.TrackSubscriptionPermissionChanged,
60 ParticipantEvent.TrackSubscriptionStatusChanged,
61];
62
63export const allParticipantEvents = [
64 ...allRemoteParticipantEvents,
65 ParticipantEvent.LocalTrackPublished,
66 ParticipantEvent.LocalTrackUnpublished,
67];
Added view/app/src/vendor/livekit/core/helper/featureDetection.ts view/app/src/vendor/livekit/core/helper/featureDetection.ts +10 −0 View view/app/src/vendor/livekit/core/helper/featureDetection.ts
@@ -0,0 +1,10 @@
1/**
2 * Returns `true` if the browser supports screen sharing.
3 */
4export function supportsScreenSharing(): boolean {
5 return (
6 typeof navigator !== 'undefined' &&
7 navigator.mediaDevices &&
8 !!navigator.mediaDevices.getDisplayMedia
9 );
10}
Added view/app/src/vendor/livekit/core/helper/floating-menu.ts view/app/src/vendor/livekit/core/helper/floating-menu.ts +22 −0 View view/app/src/vendor/livekit/core/helper/floating-menu.ts
@@ -0,0 +1,22 @@
1import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
2
3export function computeMenuPosition(
4 button: HTMLElement,
5 menu: HTMLElement,
6 onUpdate?: (x: number, y: number) => void,
7): () => void {
8 const cleanup = autoUpdate(button, menu, async () => {
9 const { x, y } = await computePosition(button, menu, {
10 placement: 'top',
11 middleware: [offset(6), flip(), shift({ padding: 5 })],
12 });
13
14 onUpdate?.(x, y);
15 });
16 return cleanup;
17}
18
19export function wasClickOutside(insideElement: HTMLElement, event: MouseEvent): boolean {
20 const isOutside = !insideElement.contains(event.target as Node);
21 return isOutside;
22}
Added view/app/src/vendor/livekit/core/helper/future.ts view/app/src/vendor/livekit/core/helper/future.ts +32 −0 View view/app/src/vendor/livekit/core/helper/future.ts
@@ -0,0 +1,32 @@
1export class Future<T, E extends Error> {
2 promise: Promise<T>;
3
4 resolve?: (arg: T) => void;
5
6 reject?: (e: E) => void;
7
8 onFinally?: () => void;
9
10 get isResolved(): boolean {
11 return this._isResolved;
12 }
13
14 private _isResolved: boolean = false;
15
16 constructor(
17 futureBase?: (resolve: (arg: T) => void, reject: (e: E) => void) => void,
18 onFinally?: () => void,
19 ) {
20 this.onFinally = onFinally;
21 this.promise = new Promise<T>(async (resolve, reject) => {
22 this.resolve = resolve;
23 this.reject = reject;
24 if (futureBase) {
25 await futureBase(resolve, reject);
26 }
27 }).finally(() => {
28 this._isResolved = true;
29 this.onFinally?.();
30 });
31 }
32}
Added view/app/src/vendor/livekit/core/helper/grid-layouts.ts view/app/src/vendor/livekit/core/helper/grid-layouts.ts +170 −0 View view/app/src/vendor/livekit/core/helper/grid-layouts.ts
@@ -0,0 +1,170 @@
1import { log } from '../logger';
2
3/**
4 * @public
5 */
6export type GridLayoutDefinition = {
7 /** Column count of the grid layout. */
8 columns: number;
9 /** Row count of the grid layout. */
10 rows: number;
11 // # Constraints that have to be meet to use this layout.
12 /**
13 * Minimum grid container width required to use this layout.
14 * @remarks
15 * If this constraint is not met, we try to select a layout with fewer tiles
16 * (`tiles=columns*rows`) that is within the constraint.
17 */
18 minWidth?: number;
19 /**
20 * Minimum grid container height required to use this layout.
21 * @remarks
22 * If this constraint is not met, we try to select a layout with fewer tiles
23 * (`tiles=columns*rows`) that is within the constraint.
24 */
25 minHeight?: number;
26 /**
27 * For which orientation the layout definition should be applied.
28 * Will be used for both landscape and portrait if no value is specified.
29 */
30 orientation?: 'landscape' | 'portrait';
31};
32
33export type GridLayoutInfo = {
34 /** Layout name (convention `<column_count>x<row_count>`). */
35 name: string;
36 /** Column count of the layout. */
37 columns: number;
38 /** Row count of the layout. */
39 rows: number;
40 // # Constraints that have to be meet to use this layout.
41 // ## 1. Participant range:
42 /** Maximum tiles that fit into this layout. */
43 maxTiles: number;
44 // ## 2. Screen size limits:
45 /** Minimum width required to use this layout. */
46 minWidth: number;
47 /** Minimum height required to use this layout. */
48 minHeight: number;
49 orientation?: 'landscape' | 'portrait';
50};
51
52export const GRID_LAYOUTS: GridLayoutDefinition[] = [
53 {
54 columns: 1,
55 rows: 1,
56 },
57 {
58 columns: 1,
59 rows: 2,
60 orientation: 'portrait',
61 },
62 {
63 columns: 2,
64 rows: 1,
65 orientation: 'landscape',
66 },
67 {
68 columns: 2,
69 rows: 2,
70 minWidth: 560,
71 },
72 {
73 columns: 3,
74 rows: 3,
75 minWidth: 700,
76 },
77 {
78 columns: 4,
79 rows: 4,
80 minWidth: 960,
81 },
82 {
83 columns: 5,
84 rows: 5,
85 minWidth: 1100,
86 },
87] as const;
88
89export function selectGridLayout(
90 layoutDefinitions: GridLayoutDefinition[],
91 participantCount: number,
92 width: number,
93 height: number,
94): GridLayoutInfo {
95 if (layoutDefinitions.length < 1) {
96 throw new Error('At least one grid layout definition must be provided.');
97 }
98 const layouts = expandAndSortLayoutDefinitions(layoutDefinitions);
99 if (width <= 0 || height <= 0) {
100 return layouts[0];
101 }
102 // Find the best layout to fit all participants.
103 let currentLayoutIndex = 0;
104 const containerOrientation = width / height > 1 ? 'landscape' : 'portrait';
105 let layout = layouts.find((layout_, index, allLayouts) => {
106 currentLayoutIndex = index;
107 const isBiggerLayoutAvailable =
108 allLayouts.findIndex((l, i) => {
109 const fitsOrientation = !l.orientation || l.orientation === containerOrientation;
110 const layoutIsBiggerThanCurrent = i > index;
111 const layoutFitsSameAmountOfParticipants = l.maxTiles === layout_.maxTiles;
112 return layoutIsBiggerThanCurrent && layoutFitsSameAmountOfParticipants && fitsOrientation;
113 }) !== -1;
114 return layout_.maxTiles >= participantCount && !isBiggerLayoutAvailable;
115 });
116 if (layout === undefined) {
117 layout = layouts[layouts.length - 1];
118 if (layout) {
119 log.warn(
120 `No layout found for: participantCount: ${participantCount}, width/height: ${width}/${height} fallback to biggest available layout (${layout}).`,
121 );
122 } else {
123 throw new Error(`No layout or fallback layout found.`);
124 }
125 }
126
127 // Check if the layout fits into the screen constraints. If not, recursively check the next smaller layout.
128 if (width < layout.minWidth || height < layout.minHeight) {
129 // const currentLayoutIndex = layouts.indexOf(layout);
130 if (currentLayoutIndex > 0) {
131 const smallerLayout = layouts[currentLayoutIndex - 1];
132 layout = selectGridLayout(
133 layouts.slice(0, currentLayoutIndex),
134 smallerLayout.maxTiles,
135 width,
136 height,
137 );
138 }
139 }
140 return layout;
141}
142
143/**
144 * @internal
145 */
146export function expandAndSortLayoutDefinitions(layouts: GridLayoutDefinition[]): GridLayoutInfo[] {
147 return [...layouts]
148 .map((layout) => {
149 return {
150 name: `${layout.columns}x${layout.rows}`,
151 columns: layout.columns,
152 rows: layout.rows,
153 maxTiles: layout.columns * layout.rows,
154 minWidth: layout.minWidth ?? 0,
155 minHeight: layout.minHeight ?? 0,
156 orientation: layout.orientation,
157 } satisfies GridLayoutInfo;
158 })
159 .sort((a, b) => {
160 if (a.maxTiles !== b.maxTiles) {
161 return a.maxTiles - b.maxTiles;
162 } else if (a.minWidth !== 0 || b.minWidth !== 0) {
163 return a.minWidth - b.minWidth;
164 } else if (a.minHeight !== 0 || b.minHeight !== 0) {
165 return a.minHeight - b.minHeight;
166 } else {
167 return 0;
168 }
169 });
170}
Added view/app/src/vendor/livekit/core/helper/index.ts view/app/src/vendor/livekit/core/helper/index.ts +15 −0 View view/app/src/vendor/livekit/core/helper/index.ts
@@ -0,0 +1,15 @@
1export * from './detectMobileBrowser';
2export * from './url-regex';
3export * from './emailRegex';
4export * from './floating-menu';
5export * from './tokenizer';
6export * from './eventGroups';
7export {
8 selectGridLayout,
9 GRID_LAYOUTS,
10 type GridLayoutDefinition,
11 type GridLayoutInfo,
12} from './grid-layouts';
13export { setDifference } from './set-helper';
14export { supportsScreenSharing } from './featureDetection';
15export * from './transcriptions';
Added view/app/src/vendor/livekit/core/helper/set-helper.ts view/app/src/vendor/livekit/core/helper/set-helper.ts +7 −0 View view/app/src/vendor/livekit/core/helper/set-helper.ts
@@ -0,0 +1,7 @@
1export function setDifference<T>(setA: Set<T>, setB: Set<T>): Set<T> {
2 const _difference = new Set(setA);
3 for (const elem of setB) {
4 _difference.delete(elem);
5 }
6 return _difference;
7}
Added view/app/src/vendor/livekit/core/helper/tokenizer.ts view/app/src/vendor/livekit/core/helper/tokenizer.ts +43 −0 View view/app/src/vendor/livekit/core/helper/tokenizer.ts
@@ -0,0 +1,43 @@
1import { createEmailRegExp } from './emailRegex';
2import { createUrlRegExp } from './url-regex';
3
4export type TokenizeGrammar = { [type: string]: RegExp };
5
6export const createDefaultGrammar = () => {
7 return {
8 email: createEmailRegExp(),
9 url: createUrlRegExp({}),
10 } satisfies TokenizeGrammar;
11};
12
13export function tokenize<T extends TokenizeGrammar>(input: string, grammar: T) {
14 const matches = Object.entries(grammar)
15 .map(([type, rx], weight) =>
16 Array.from(input.matchAll(rx)).map(({ index, 0: content }) => ({
17 type: type as keyof T,
18 weight,
19 content,
20 index: index ?? 0,
21 })),
22 )
23 .flat()
24 .sort((a, b) => {
25 const d = a.index - b.index;
26 return d !== 0 ? d : a.weight - b.weight;
27 })
28 .filter(({ index }, i, arr) => {
29 if (i === 0) return true;
30 const prev = arr[i - 1];
31 return prev.index + prev.content.length <= index;
32 });
33
34 const tokens = [];
35 let pos = 0;
36 for (const { type, content, index } of matches) {
37 if (index > pos) tokens.push(input.substring(pos, index));
38 tokens.push({ type, content });
39 pos = index + content.length;
40 }
41 if (input.length > pos) tokens.push(input.substring(pos));
42 return tokens;
43}
Added view/app/src/vendor/livekit/core/helper/transcriptions.ts view/app/src/vendor/livekit/core/helper/transcriptions.ts +75 −0 View view/app/src/vendor/livekit/core/helper/transcriptions.ts
@@ -0,0 +1,75 @@
1import type { TranscriptionSegment } from 'livekit-client';
2
3export type ReceivedTranscriptionSegment = TranscriptionSegment & {
4 receivedAtMediaTimestamp: number;
5 receivedAt: number;
6};
7
8export function getActiveTranscriptionSegments(
9 segments: ReceivedTranscriptionSegment[],
10 syncTimes: { timestamp: number; rtpTimestamp?: number },
11 maxAge = 0,
12) {
13 return segments.filter((segment) => {
14 const hasTrackSync = !!syncTimes.rtpTimestamp;
15 const currentTrackTime = syncTimes.rtpTimestamp ?? performance.timeOrigin + performance.now();
16 // if a segment arrives late, consider startTime to be the media timestamp from when the segment was received client side
17 const displayStartTime = hasTrackSync
18 ? Math.max(segment.receivedAtMediaTimestamp, segment.startTime)
19 : segment.receivedAt;
20 // "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
21 const segmentDuration = maxAge + segment.endTime - segment.startTime;
22 return (
23 currentTrackTime >= displayStartTime && currentTrackTime <= displayStartTime + segmentDuration
24 );
25 });
26}
27
28export function addMediaTimestampToTranscription(
29 segment: TranscriptionSegment,
30 timestamps: { timestamp: number; rtpTimestamp?: number },
31): ReceivedTranscriptionSegment {
32 return {
33 ...segment,
34 receivedAtMediaTimestamp: timestamps.rtpTimestamp ?? 0,
35 receivedAt: timestamps.timestamp,
36 };
37}
38
39/**
40 * @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
41 */
42export function dedupeSegments<T extends TranscriptionSegment>(
43 prevSegments: T[],
44 newSegments: T[],
45 windowSize: number,
46) {
47 return [...prevSegments, ...newSegments]
48 .reduceRight((acc, segment) => {
49 if (!acc.find((val) => val.id === segment.id)) {
50 acc.unshift(segment);
51 }
52 return acc;
53 }, [] as Array<T>)
54 .slice(0 - windowSize);
55}
56
57export function didActiveSegmentsChange<T extends TranscriptionSegment>(
58 prevActive: T[],
59 newActive: T[],
60) {
61 if (newActive.length !== prevActive.length) {
62 return true;
63 }
64 return !newActive.every((newSegment) => {
65 return prevActive.find(
66 (prevSegment) =>
67 prevSegment.id === newSegment.id &&
68 prevSegment.text === newSegment.text &&
69 prevSegment.final === newSegment.final &&
70 prevSegment.language === newSegment.language &&
71 prevSegment.startTime === newSegment.startTime &&
72 prevSegment.endTime === newSegment.endTime,
73 );
74 });
75}
Added view/app/src/vendor/livekit/core/helper/url-regex.ts view/app/src/vendor/livekit/core/helper/url-regex.ts +50 −0 View view/app/src/vendor/livekit/core/helper/url-regex.ts
@@ -0,0 +1,50 @@
1// The MIT License (MIT)
2
3// Copyright (c) Kevin Mårtensson <kevinmartensson@gmail.com> and Diego Perini
4
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11
12// The above copyright notice and this permission notice shall be included in
13// all copies or substantial portions of the Software.
14
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21// THE SOFTWARE.
22
23interface RegExOptions {
24 /**
25 Only match an exact string. Useful with `RegExp#test` to check if a string is a URL.
26 @defaultValue false
27 */
28 readonly exact?: boolean;
29}
30
31export function createUrlRegExp(options: RegExOptions) {
32 options = {
33 ...options,
34 };
35
36 const protocol = `(?:(?:[a-z]+:)?//)?`;
37 const auth = '(?:\\S+(?::\\S*)?@)?';
38 const ip = new RegExp(
39 '(?: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}',
40 'g',
41 ).source;
42 const host = '(?:(?:[a-z\\u00a1-\\uffff0-9][-_]*)*[a-z\\u00a1-\\uffff0-9]+)';
43 const domain = '(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*';
44 const tld = `(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))\\.?`;
45 const port = '(?::\\d{2,5})?';
46 const path = '(?:[/?#][^\\s"]*)?';
47 const regex = `(?:${protocol}|www\\.)${auth}(?:localhost|${ip}|${host}${domain}${tld})${port}${path}`;
48
49 return options.exact ? new RegExp(`(?:^${regex}$)`, 'i') : new RegExp(regex, 'ig');
50}
Added view/app/src/vendor/livekit/core/messages/index.ts view/app/src/vendor/livekit/core/messages/index.ts +1 −0 View view/app/src/vendor/livekit/core/messages/index.ts
@@ -0,0 +1 @@
1export * from './types';
Added view/app/src/vendor/livekit/core/messages/types.ts view/app/src/vendor/livekit/core/messages/types.ts +49 −0 View view/app/src/vendor/livekit/core/messages/types.ts
@@ -0,0 +1,49 @@
1import type { Participant, ChatMessage } from 'livekit-client';
2
3/** @public */
4export type { ChatMessage };
5
6export type SentMessage = ChatMessage;
7
8type ReceivedMessageWithType<Type extends string, Metadata extends object = object> = {
9 id: string;
10 timestamp: number;
11
12 type: Type;
13
14 from?: Participant;
15 attributes?: Record<string, string>;
16} & Metadata;
17
18/** @private */
19type ReceivedChatMessageWithRequiredType = ReceivedMessageWithType<
20 'chatMessage',
21 ChatMessage & {
22 from?: Participant;
23 attributes?: Record<string, string>;
24 }
25>;
26
27/** @public */
28export type ReceivedChatMessage = Omit<ReceivedChatMessageWithRequiredType, 'type'> & {
29 type?: 'chatMessage';
30};
31
32export type ReceivedUserTranscriptionMessage = ReceivedMessageWithType<
33 'userTranscript',
34 {
35 message: string;
36 }
37>;
38
39export type ReceivedAgentTranscriptionMessage = ReceivedMessageWithType<
40 'agentTranscript',
41 {
42 message: string;
43 }
44>;
45
46/** @beta */
47export type ReceivedMessage =
48 ReceivedUserTranscriptionMessage | ReceivedAgentTranscriptionMessage | ReceivedChatMessage;
49// TODO: images? attachments? rpc?
Added view/app/src/vendor/livekit/core/observables/dataChannel.ts view/app/src/vendor/livekit/core/observables/dataChannel.ts +113 −0 View view/app/src/vendor/livekit/core/observables/dataChannel.ts
@@ -0,0 +1,113 @@
1import {
2 type ChatMessage,
3 type DataPublishOptions,
4 type LocalParticipant,
5 type Participant,
6 type Room,
7 type SendTextOptions,
8} from 'livekit-client';
9import type { Subscriber } from 'rxjs';
10import { Observable, filter, map } from 'rxjs';
11import { createChatObserver, createDataObserver } from './room';
12import { type ReceivedChatMessage } from '../components/chat';
13
14export const DataTopic = {
15 CHAT: 'lk.chat',
16 TRANSCRIPTION: 'lk.transcription',
17} as const;
18
19/** @deprecated */
20export const LegacyDataTopic = {
21 CHAT: 'lk-chat-topic',
22} as const;
23
24/** Publish data from the LocalParticipant. */
25export async function sendMessage(
26 localParticipant: LocalParticipant,
27 payload: Uint8Array,
28 options: DataPublishOptions = {},
29) {
30 const { reliable, destinationIdentities, topic } = options;
31
32 // `publishData` requires the non-shared `Uint8Array<ArrayBuffer>` variant (livekit-client >=2.19),
33 // but this helper intentionally accepts any consumer-supplied `Uint8Array` and forwards it
34 // unchanged (as it did before that type tightening). Data-channel payloads are not shared-backed.
35 await localParticipant.publishData(payload as Uint8Array<ArrayBuffer>, {
36 destinationIdentities,
37 topic,
38 reliable,
39 });
40}
41
42export interface BaseDataMessage<T extends string | undefined> {
43 topic?: T;
44 payload: Uint8Array;
45}
46
47export interface ReceivedDataMessage<
48 T extends string | undefined = string,
49> extends BaseDataMessage<T> {
50 from?: Participant;
51}
52
53export function setupDataMessageHandler<T extends string>(
54 room: Room,
55 topic?: T | [T, ...T[]],
56 onMessage?: (msg: ReceivedDataMessage<T>) => void,
57) {
58 const topics = Array.isArray(topic) ? topic : [topic];
59 /** Setup a Observable that returns all data messages belonging to a topic. */
60 const messageObservable = createDataObserver(room).pipe(
61 filter(
62 ([, , , messageTopic]) =>
63 topic === undefined || (messageTopic !== undefined && topics.includes(messageTopic as T)),
64 ),
65 map(([payload, participant, , messageTopic]) => {
66 const msg = {
67 payload,
68 topic: messageTopic as T,
69 from: participant,
70 } satisfies ReceivedDataMessage<T>;
71 onMessage?.(msg);
72 return msg;
73 }),
74 );
75
76 let isSendingSubscriber: Subscriber<boolean>;
77 const isSendingObservable = new Observable<boolean>((subscriber) => {
78 isSendingSubscriber = subscriber;
79 });
80
81 const send = async (payload: Uint8Array, options: DataPublishOptions = {}) => {
82 isSendingSubscriber.next(true);
83 try {
84 await sendMessage(room.localParticipant, payload, { topic: topics[0], ...options });
85 } finally {
86 isSendingSubscriber.next(false);
87 }
88 };
89
90 return { messageObservable, isSendingObservable, send };
91}
92
93export function setupChatMessageHandler(room: Room) {
94 const chatObservable = createChatObserver(room);
95
96 const send = async (text: string, options: SendTextOptions): Promise<ReceivedChatMessage> => {
97 const msg = await room.localParticipant.sendChatMessage(text, options);
98 await room.localParticipant.sendText(text, options);
99 return {
100 ...msg,
101 type: 'chatMessage',
102 from: room.localParticipant,
103 attachedFiles: options.attachments,
104 };
105 };
106
107 const edit = async (text: string, originalMsg: ChatMessage) => {
108 const msg = await room.localParticipant.editChatMessage(text, originalMsg);
109 return msg;
110 };
111
112 return { chatObservable, send, edit };
113}
Added view/app/src/vendor/livekit/core/observables/dom-event.ts view/app/src/vendor/livekit/core/observables/dom-event.ts +21 −0 View view/app/src/vendor/livekit/core/observables/dom-event.ts
@@ -0,0 +1,21 @@
1import type { Observable } from 'rxjs';
2import { concat, distinctUntilChanged, fromEvent, map, of, skipUntil, timeout } from 'rxjs';
3
4/**
5 * Returns true if the user is interacting with the HTML element,
6 * and returns false if there is no interaction for a specified period of time.
7 *
8 * @internal
9 */
10export function createInteractingObservable(htmlElement: HTMLElement | null, inactiveAfter = 1000) {
11 if (htmlElement === null) return of(false);
12 const move$ = fromEvent(htmlElement, 'mousemove', { passive: true }).pipe(map(() => true));
13 const moveAndStop$: Observable<boolean> = move$.pipe(
14 timeout({
15 each: inactiveAfter,
16 with: () => concat(of(false), moveAndStop$.pipe(skipUntil(move$))),
17 }),
18 distinctUntilChanged(),
19 );
20 return moveAndStop$;
21}
Added view/app/src/vendor/livekit/core/observables/participant.ts view/app/src/vendor/livekit/core/observables/participant.ts +321 −0 View view/app/src/vendor/livekit/core/observables/participant.ts
@@ -0,0 +1,321 @@
1import type { ParticipantPermission } from '@livekit/protocol';
2import {
3 Participant,
4 RemoteParticipant,
5 Room,
6 TrackPublication,
7 ParticipantEvent,
8 RoomEvent,
9 Track,
10 type ParticipantEventCallbacks,
11} from 'livekit-client';
12import type { Subscriber } from 'rxjs';
13import { Observable, map, startWith, switchMap } from 'rxjs';
14import { getTrackByIdentifier } from '../components/mediaTrack';
15import { allParticipantEvents, allParticipantRoomEvents } from '../helper/eventGroups';
16import type { TrackReferenceOrPlaceholder } from '../track-reference';
17import type { ParticipantIdentifier, TrackIdentifier } from '../types';
18import { observeRoomEvents } from './room';
19
20export function observeParticipantEvents<T extends Participant>(
21 participant: T,
22 ...events: ParticipantEvent[]
23) {
24 const observable = new Observable<T>((subscribe) => {
25 const onParticipantUpdate = () => {
26 subscribe.next(participant);
27 };
28
29 events.forEach((evt) => {
30 participant.on(evt as keyof ParticipantEventCallbacks, onParticipantUpdate);
31 });
32
33 const unsubscribe = () => {
34 events.forEach((evt) => {
35 participant.off(evt as keyof ParticipantEventCallbacks, onParticipantUpdate);
36 });
37 };
38 return unsubscribe;
39 }).pipe(startWith(participant));
40
41 return observable;
42}
43
44export interface ParticipantMedia<T extends Participant = Participant> {
45 isCameraEnabled: boolean;
46 isMicrophoneEnabled: boolean;
47 isScreenShareEnabled: boolean;
48 microphoneTrack?: TrackPublication;
49 cameraTrack?: TrackPublication;
50 participant: T;
51}
52
53export function observeParticipantMedia<T extends Participant>(participant: T) {
54 const participantObserver = observeParticipantEvents(
55 participant,
56 ParticipantEvent.TrackMuted,
57 ParticipantEvent.TrackUnmuted,
58 ParticipantEvent.ParticipantPermissionsChanged,
59 // ParticipantEvent.IsSpeakingChanged,
60 ParticipantEvent.TrackPublished,
61 ParticipantEvent.TrackUnpublished,
62 ParticipantEvent.LocalTrackPublished,
63 ParticipantEvent.LocalTrackUnpublished,
64 ParticipantEvent.MediaDevicesError,
65 ParticipantEvent.TrackSubscriptionStatusChanged,
66 // ParticipantEvent.ConnectionQualityChanged,
67 ).pipe(
68 map((p) => {
69 const { isMicrophoneEnabled, isCameraEnabled, isScreenShareEnabled } = p;
70 const microphoneTrack = p.getTrackPublication(Track.Source.Microphone);
71 const cameraTrack = p.getTrackPublication(Track.Source.Camera);
72 const participantMedia: ParticipantMedia<T> = {
73 isCameraEnabled,
74 isMicrophoneEnabled,
75 isScreenShareEnabled,
76 cameraTrack,
77 microphoneTrack,
78 participant: p,
79 };
80 return participantMedia;
81 }),
82 );
83
84 return participantObserver;
85}
86
87export function createTrackObserver(participant: Participant, options: TrackIdentifier) {
88 return observeParticipantMedia(participant).pipe(
89 map(() => {
90 return { publication: getTrackByIdentifier(options) };
91 }),
92 );
93}
94
95export function participantInfoObserver(participant?: Participant) {
96 if (!participant) {
97 return undefined;
98 }
99 const observer = observeParticipantEvents(
100 participant,
101 ParticipantEvent.ParticipantMetadataChanged,
102 ParticipantEvent.ParticipantNameChanged,
103 ).pipe(
104 map(({ name, identity, metadata }) => {
105 return {
106 name,
107 identity,
108 metadata,
109 };
110 }),
111 startWith({
112 name: participant.name,
113 identity: participant.identity,
114 metadata: participant.metadata,
115 }),
116 );
117 return observer;
118}
119
120export function createConnectionQualityObserver(participant: Participant) {
121 const observer = participantEventSelector(
122 participant,
123 ParticipantEvent.ConnectionQualityChanged,
124 ).pipe(
125 map(([quality]) => quality),
126 startWith(participant.connectionQuality),
127 );
128 return observer;
129}
130
131export function participantEventSelector<T extends ParticipantEvent>(
132 participant: Participant,
133 event: T,
134) {
135 const observable = new Observable<
136 Parameters<ParticipantEventCallbacks[Extract<T, keyof ParticipantEventCallbacks>]>
137 >((subscribe) => {
138 const update = (
139 ...params: Parameters<ParticipantEventCallbacks[Extract<T, keyof ParticipantEventCallbacks>]>
140 ) => {
141 subscribe.next(params);
142 };
143 // @ts-expect-error not a perfect overlap between ParticipantEvent and keyof ParticipantEventCallbacks
144 participant.on(event, update);
145
146 const unsubscribe = () => {
147 // @ts-expect-error not a perfect overlap between ParticipantEvent and keyof ParticipantEventCallbacks
148 participant.off(event, update);
149 };
150 return unsubscribe;
151 });
152
153 return observable;
154}
155
156export function mutedObserver(trackRef: TrackReferenceOrPlaceholder) {
157 return observeParticipantEvents(
158 trackRef.participant,
159 ParticipantEvent.TrackMuted,
160 ParticipantEvent.TrackUnmuted,
161 ParticipantEvent.TrackSubscribed,
162 ParticipantEvent.TrackUnsubscribed,
163 ParticipantEvent.LocalTrackPublished,
164 ParticipantEvent.LocalTrackUnpublished,
165 ).pipe(
166 map((participant) => {
167 const pub = trackRef.publication ?? participant.getTrackPublication(trackRef.source);
168 return pub?.isMuted ?? true;
169 }),
170 startWith(
171 trackRef.publication?.isMuted ??
172 trackRef.participant.getTrackPublication(trackRef.source)?.isMuted ??
173 true,
174 ),
175 );
176}
177
178export function createIsSpeakingObserver(participant: Participant) {
179 return participantEventSelector(participant, ParticipantEvent.IsSpeakingChanged).pipe(
180 map(([isSpeaking]) => isSpeaking),
181 );
182}
183
184type ConnectedParticipantsObserverOptions = {
185 additionalRoomEvents?: RoomEvent[];
186};
187
188export function connectedParticipantsObserver(
189 room: Room,
190 options: ConnectedParticipantsObserverOptions = {},
191) {
192 let subscriber: Subscriber<RemoteParticipant[]> | undefined;
193
194 const observable = new Observable<RemoteParticipant[]>((sub) => {
195 subscriber = sub;
196 return () => listener.unsubscribe();
197 }).pipe(startWith(Array.from(room.remoteParticipants.values())));
198
199 const additionalRoomEvents = options.additionalRoomEvents ?? allParticipantRoomEvents;
200
201 const roomEvents = Array.from(
202 new Set([
203 RoomEvent.ParticipantConnected,
204 RoomEvent.ParticipantDisconnected,
205 RoomEvent.ConnectionStateChanged,
206 ...additionalRoomEvents,
207 ]),
208 );
209
210 const listener = observeRoomEvents(room, ...roomEvents).subscribe((r) =>
211 subscriber?.next(Array.from(r.remoteParticipants.values())),
212 );
213 if (room.remoteParticipants.size > 0) {
214 subscriber?.next(Array.from(room.remoteParticipants.values()));
215 }
216 return observable;
217}
218
219export type ConnectedParticipantObserverOptions = {
220 additionalEvents?: ParticipantEvent[];
221};
222
223export function connectedParticipantObserver(
224 room: Room,
225 identity: string,
226 options: ConnectedParticipantObserverOptions = {},
227) {
228 const additionalEvents = options.additionalEvents ?? allParticipantEvents;
229 const observable = observeRoomEvents(
230 room,
231 RoomEvent.ParticipantConnected,
232 RoomEvent.ParticipantDisconnected,
233 RoomEvent.ConnectionStateChanged,
234 ).pipe(
235 switchMap((r) => {
236 const participant = r.getParticipantByIdentity(identity) as RemoteParticipant | undefined;
237 if (participant) {
238 return observeParticipantEvents(participant, ...additionalEvents);
239 } else {
240 return new Observable<undefined>((subscribe) => subscribe.next(undefined));
241 }
242 }),
243 startWith(room.getParticipantByIdentity(identity) as RemoteParticipant | undefined),
244 );
245
246 return observable;
247}
248
249export function participantPermissionObserver(
250 participant: Participant,
251): Observable<ParticipantPermission | undefined> {
252 const observer = participantEventSelector(
253 participant,
254 ParticipantEvent.ParticipantPermissionsChanged,
255 ).pipe(
256 map(() => participant.permissions),
257 startWith(participant.permissions),
258 );
259 return observer;
260}
261
262export function participantByIdentifierObserver(
263 room: Room,
264 { kind, identity }: ParticipantIdentifier,
265 options: ConnectedParticipantObserverOptions = {},
266): Observable<RemoteParticipant | undefined> {
267 const additionalEvents = options.additionalEvents ?? allParticipantEvents;
268 const matchesIdentifier = (participant: RemoteParticipant) => {
269 let isMatch = true;
270 if (kind) {
271 isMatch = isMatch && participant.kind === kind;
272 }
273 if (identity) {
274 isMatch = isMatch && participant.identity === identity;
275 }
276 return isMatch;
277 };
278 const observable = observeRoomEvents(
279 room,
280 RoomEvent.ParticipantConnected,
281 RoomEvent.ParticipantDisconnected,
282 RoomEvent.ConnectionStateChanged,
283 ).pipe(
284 switchMap((r) => {
285 const participant = Array.from(r.remoteParticipants.values()).find((p) =>
286 matchesIdentifier(p),
287 );
288 if (participant) {
289 return observeParticipantEvents(participant, ...additionalEvents);
290 } else {
291 return new Observable<undefined>((subscribe) => subscribe.next(undefined));
292 }
293 }),
294 startWith(Array.from(room.remoteParticipants.values()).find((p) => matchesIdentifier(p))),
295 );
296
297 return observable;
298}
299
300export function participantAttributesObserver(participant: Participant): Observable<{
301 changed: Readonly<Record<string, string>>;
302 attributes: Readonly<Record<string, string>>;
303}>;
304export function participantAttributesObserver(participant: undefined): Observable<{
305 changed: undefined;
306 attributes: undefined;
307}>;
308export function participantAttributesObserver(participant: Participant | undefined) {
309 if (typeof participant === 'undefined') {
310 return new Observable<{ changed: undefined; attributes: undefined }>();
311 }
312 return participantEventSelector(participant, ParticipantEvent.AttributesChanged).pipe(
313 map(([changedAttributes]) => {
314 return {
315 changed: changedAttributes as Readonly<Record<string, string>>,
316 attributes: participant.attributes,
317 };
318 }),
319 startWith({ changed: participant.attributes, attributes: participant.attributes }),
320 );
321}
Added view/app/src/vendor/livekit/core/observables/room.ts view/app/src/vendor/livekit/core/observables/room.ts +291 −0 View view/app/src/vendor/livekit/core/observables/room.ts
@@ -0,0 +1,291 @@
1import {
2 LocalParticipant,
3 Room,
4 RoomEvent,
5 Track,
6 type Participant,
7 type TrackPublication,
8 type RoomEventCallbacks,
9} from 'livekit-client';
10import type { Subscriber, Subscription } from 'rxjs';
11import { concat, filter, map, Observable, startWith } from 'rxjs';
12import { log } from '../logger';
13export function observeRoomEvents(room: Room, ...events: RoomEvent[]): Observable<Room> {
14 const observable = new Observable<Room>((subscribe) => {
15 const onRoomUpdate = () => {
16 subscribe.next(room);
17 };
18
19 events.forEach((evt) => {
20 room.on(evt, onRoomUpdate);
21 });
22
23 const unsubscribe = () => {
24 events.forEach((evt) => {
25 room.off(evt, onRoomUpdate);
26 });
27 };
28 return unsubscribe;
29 }).pipe(startWith(room));
30
31 return observable;
32}
33
34export function roomEventSelector<T extends RoomEvent>(room: Room, event: T) {
35 const observable = new Observable<Parameters<RoomEventCallbacks[T]>>((subscribe) => {
36 const update = (...params: Parameters<RoomEventCallbacks[T]>) => {
37 subscribe.next(params);
38 };
39 room.on(event as keyof RoomEventCallbacks, update);
40
41 const unsubscribe = () => {
42 room.off(event as keyof RoomEventCallbacks, update);
43 };
44 return unsubscribe;
45 });
46
47 return observable;
48}
49
50export function roomObserver(room: Room) {
51 const observable = observeRoomEvents(
52 room,
53 RoomEvent.ParticipantConnected,
54 RoomEvent.ParticipantDisconnected,
55 RoomEvent.ActiveSpeakersChanged,
56 RoomEvent.TrackSubscribed,
57 RoomEvent.TrackUnsubscribed,
58 RoomEvent.LocalTrackPublished,
59 RoomEvent.LocalTrackUnpublished,
60 RoomEvent.AudioPlaybackStatusChanged,
61 RoomEvent.ConnectionStateChanged,
62 );
63
64 return observable;
65}
66
67export function connectionStateObserver(room: Room) {
68 return roomEventSelector(room, RoomEvent.ConnectionStateChanged).pipe(
69 map(([connectionState]) => connectionState),
70 startWith(room.state),
71 );
72}
73export type ScreenShareTrackMap = Array<{
74 participant: Participant;
75 tracks: Array<TrackPublication>;
76}>;
77
78export function screenShareObserver(room: Room) {
79 let screenShareSubscriber: Subscriber<ScreenShareTrackMap>;
80 const observers: Array<Subscription> = [];
81
82 const observable = new Observable<ScreenShareTrackMap>((subscriber) => {
83 screenShareSubscriber = subscriber;
84 return () => {
85 observers.forEach((observer) => {
86 observer.unsubscribe();
87 });
88 };
89 });
90 const screenShareTracks: ScreenShareTrackMap = [];
91
92 const handleSub = (publication: TrackPublication, participant: Participant) => {
93 if (
94 publication.source !== Track.Source.ScreenShare &&
95 publication.source !== Track.Source.ScreenShareAudio
96 ) {
97 return;
98 }
99 let trackMap = screenShareTracks.find((tr) => tr.participant.identity === participant.identity);
100 const getScreenShareTracks = (participant: Participant) => {
101 return participant
102 .getTrackPublications()
103 .filter(
104 (track) =>
105 (track.source === Track.Source.ScreenShare ||
106 track.source === Track.Source.ScreenShareAudio) &&
107 track.track,
108 );
109 };
110 if (!trackMap) {
111 trackMap = {
112 participant,
113 tracks: getScreenShareTracks(participant),
114 };
115 } else {
116 const index = screenShareTracks.indexOf(trackMap);
117 screenShareTracks.splice(index, 1);
118 trackMap.tracks = getScreenShareTracks(participant);
119 }
120 if (trackMap.tracks.length > 0) {
121 screenShareTracks.push(trackMap);
122 }
123
124 screenShareSubscriber.next(screenShareTracks);
125 };
126 observers.push(
127 roomEventSelector(room, RoomEvent.TrackSubscribed).subscribe(([, ...args]) =>
128 handleSub(...args),
129 ),
130 );
131 observers.push(
132 roomEventSelector(room, RoomEvent.TrackUnsubscribed).subscribe(([, ...args]) =>
133 handleSub(...args),
134 ),
135 );
136 observers.push(
137 roomEventSelector(room, RoomEvent.LocalTrackPublished).subscribe((args) => handleSub(...args)),
138 );
139 observers.push(
140 roomEventSelector(room, RoomEvent.LocalTrackUnpublished).subscribe((args) => {
141 handleSub(...args);
142 }),
143 );
144 observers.push(
145 roomEventSelector(room, RoomEvent.TrackMuted).subscribe((args) => {
146 handleSub(...args);
147 }),
148 );
149 observers.push(
150 roomEventSelector(room, RoomEvent.TrackUnmuted).subscribe((args) => {
151 handleSub(...args);
152 }),
153 );
154 setTimeout(() => {
155 // TODO find way to avoid this timeout
156 for (const p of room.remoteParticipants.values()) {
157 p.getTrackPublications().forEach((track) => {
158 handleSub(track, p);
159 });
160 }
161 }, 1);
162
163 return observable;
164}
165
166export function roomInfoObserver(room: Room) {
167 const observer = observeRoomEvents(
168 room,
169 RoomEvent.RoomMetadataChanged,
170 RoomEvent.ConnectionStateChanged,
171 ).pipe(
172 map((r) => {
173 return { name: r.name, metadata: r.metadata };
174 }),
175 );
176 return observer;
177}
178
179export function activeSpeakerObserver(room: Room) {
180 return roomEventSelector(room, RoomEvent.ActiveSpeakersChanged).pipe(
181 map(([speakers]) => speakers),
182 );
183}
184
185export function createMediaDeviceObserver(
186 kind?: MediaDeviceKind,
187 onError?: (e: Error) => void,
188 requestPermissions = true,
189) {
190 // Initial devices fetch observable
191 const initialDevices$ = new Observable<MediaDeviceInfo[]>((subscriber) => {
192 Room.getLocalDevices(kind, requestPermissions)
193 .then((devices) => {
194 subscriber.next(devices);
195 subscriber.complete();
196 })
197 .catch((e) => {
198 onError?.(e);
199 subscriber.next([] as MediaDeviceInfo[]);
200 subscriber.complete();
201 });
202 });
203
204 // Device change observable
205 const deviceChanges$ = new Observable<MediaDeviceInfo[]>((subscriber) => {
206 const onDeviceChange = async () => {
207 try {
208 const newDevices = await Room.getLocalDevices(kind, requestPermissions);
209 subscriber.next(newDevices);
210 } catch (e: any) {
211 onError?.(e);
212 }
213 };
214
215 if (typeof window !== 'undefined') {
216 if (!window.isSecureContext) {
217 throw new Error(
218 `Accessing media devices is available only in secure contexts (HTTPS and localhost), in some or all supporting browsers. See: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/mediaDevices`,
219 );
220 }
221 navigator?.mediaDevices?.addEventListener('devicechange', onDeviceChange);
222 }
223
224 // Return unsubscribe function that cleans up when the observable is unsubscribed
225 return () => {
226 navigator?.mediaDevices?.removeEventListener('devicechange', onDeviceChange);
227 };
228 });
229
230 // Combine both observables
231 return concat(initialDevices$, deviceChanges$);
232}
233
234export function createDataObserver(room: Room) {
235 return roomEventSelector(room, RoomEvent.DataReceived);
236}
237
238export function createChatObserver(room: Room) {
239 return roomEventSelector(room, RoomEvent.ChatMessage);
240}
241
242export function roomAudioPlaybackAllowedObservable(room: Room) {
243 const observable = observeRoomEvents(room, RoomEvent.AudioPlaybackStatusChanged).pipe(
244 map((room) => {
245 return { canPlayAudio: room.canPlaybackAudio };
246 }),
247 );
248 return observable;
249}
250
251export function roomVideoPlaybackAllowedObservable(room: Room) {
252 const observable = observeRoomEvents(room, RoomEvent.VideoPlaybackStatusChanged).pipe(
253 map((room) => {
254 return { canPlayVideo: room.canPlaybackVideo };
255 }),
256 );
257 return observable;
258}
259
260export function createActiveDeviceObservable(room: Room, kind: MediaDeviceKind) {
261 return roomEventSelector(room, RoomEvent.ActiveDeviceChanged).pipe(
262 filter(([kindOfDevice]) => kindOfDevice === kind),
263 map(([kind, deviceId]) => {
264 log.debug('activeDeviceObservable | RoomEvent.ActiveDeviceChanged', { kind, deviceId });
265 return deviceId;
266 }),
267 );
268}
269
270export function encryptionStatusObservable(room: Room, participant: Participant | undefined) {
271 return roomEventSelector(room, RoomEvent.ParticipantEncryptionStatusChanged).pipe(
272 filter(
273 ([, p]) =>
274 participant?.identity === p?.identity ||
275 (!p && participant?.identity === room.localParticipant.identity),
276 ),
277 map(([encrypted]) => encrypted),
278 startWith(
279 participant?.isLocal
280 ? (participant as LocalParticipant).isE2EEEnabled
281 : !!participant?.isEncrypted,
282 ),
283 );
284}
285
286export function recordingStatusObservable(room: Room) {
287 return roomEventSelector(room, RoomEvent.RecordingStatusChanged).pipe(
288 map(([recording]) => recording),
289 startWith(room.isRecording),
290 );
291}
Added view/app/src/vendor/livekit/core/observables/track.ts view/app/src/vendor/livekit/core/observables/track.ts +208 −0 View view/app/src/vendor/livekit/core/observables/track.ts
@@ -0,0 +1,208 @@
1import type {
2 LocalTrackPublication,
3 Participant,
4 RemoteTrackPublication,
5 Room,
6 Track,
7 TrackPublication,
8 PublicationEventCallbacks,
9} from 'livekit-client';
10import { RoomEvent, TrackEvent } from 'livekit-client';
11import { map, Observable, startWith } from 'rxjs';
12import { allParticipantRoomEvents, participantTrackEvents } from '../helper';
13import { log } from '../logger';
14import type { TrackReference } from '../track-reference';
15import { observeRoomEvents } from './room';
16import type { ParticipantTrackIdentifier } from '../types';
17import { observeParticipantEvents } from './participant';
18
19export function trackObservable(track: TrackPublication) {
20 const trackObserver = observeTrackEvents(
21 track,
22 TrackEvent.Muted,
23 TrackEvent.Unmuted,
24 TrackEvent.Subscribed,
25 TrackEvent.Unsubscribed,
26 );
27
28 return trackObserver;
29}
30
31export function observeTrackEvents(track: TrackPublication, ...events: TrackEvent[]) {
32 const observable = new Observable<TrackPublication>((subscribe) => {
33 const onTrackUpdate = () => {
34 subscribe.next(track);
35 };
36
37 events.forEach((evt) => {
38 // @ts-expect-error type of `TrackEvent` and `PublicationCallbacks` are congruent
39 track.on(evt, onTrackUpdate);
40 });
41
42 const unsubscribe = () => {
43 events.forEach((evt) => {
44 // @ts-expect-error type of `TrackEvent` and `PublicationCallbacks` are congruent
45 track.off(evt, onTrackUpdate);
46 });
47 };
48 return unsubscribe;
49 }).pipe(startWith(track));
50
51 return observable;
52}
53
54/**
55 * Create `TrackReferences` for all tracks that are included in the sources property.
56 * */
57function getTrackReferences(
58 room: Room,
59 sources: Track.Source[],
60 onlySubscribedTracks = true,
61): { trackReferences: TrackReference[]; participants: Participant[] } {
62 const localParticipant = room.localParticipant;
63 const allParticipants = [localParticipant, ...Array.from(room.remoteParticipants.values())];
64 const trackReferences: TrackReference[] = [];
65
66 allParticipants.forEach((participant) => {
67 sources.forEach((source) => {
68 const sourceReferences = Array.from<RemoteTrackPublication | LocalTrackPublication>(
69 participant.trackPublications.values(),
70 )
71 .filter(
72 (track) =>
73 track.source === source &&
74 // either return all or only the ones that are subscribed
75 (!onlySubscribedTracks || track.track),
76 )
77 .map((track): TrackReference => {
78 return {
79 participant: participant,
80 publication: track,
81 source: track.source,
82 };
83 });
84
85 trackReferences.push(...sourceReferences);
86 });
87 });
88
89 return { trackReferences, participants: allParticipants };
90}
91
92/**
93 * Create `TrackReferences` for all tracks that are included in the sources property.
94 * */
95function getParticipantTrackRefs(
96 participant: Participant,
97 identifier: ParticipantTrackIdentifier,
98 onlySubscribedTracks = false,
99): TrackReference[] {
100 const { sources, kind, name } = identifier;
101 const sourceReferences = Array.from(participant.trackPublications.values())
102 .filter(
103 (pub) =>
104 (!sources || sources.includes(pub.source)) &&
105 (!kind || pub.kind === kind) &&
106 (!name || pub.trackName === name) &&
107 // either return all or only the ones that are subscribed
108 (!onlySubscribedTracks || pub.track),
109 )
110 .map((track): TrackReference => {
111 return {
112 participant: participant,
113 publication: track,
114 source: track.source,
115 };
116 });
117
118 return sourceReferences;
119}
120
121type TrackReferencesObservableOptions = {
122 additionalRoomEvents?: RoomEvent[];
123 onlySubscribed?: boolean;
124};
125
126export function trackReferencesObservable(
127 room: Room,
128 sources: Track.Source[],
129 options: TrackReferencesObservableOptions,
130): Observable<{ trackReferences: TrackReference[]; participants: Participant[] }> {
131 const additionalRoomEvents = options.additionalRoomEvents ?? allParticipantRoomEvents;
132 const onlySubscribedTracks: boolean = options.onlySubscribed ?? true;
133 const roomEvents = Array.from(
134 new Set([
135 RoomEvent.ParticipantConnected,
136 RoomEvent.ParticipantDisconnected,
137 RoomEvent.ConnectionStateChanged,
138 RoomEvent.LocalTrackPublished,
139 RoomEvent.LocalTrackUnpublished,
140 RoomEvent.TrackPublished,
141 RoomEvent.TrackUnpublished,
142 RoomEvent.TrackSubscriptionStatusChanged,
143 ...additionalRoomEvents,
144 ]).values(),
145 );
146
147 const observable = observeRoomEvents(room, ...roomEvents).pipe(
148 map((room) => {
149 const data = getTrackReferences(room, sources, onlySubscribedTracks);
150 log.debug(`TrackReference[] was updated. (length ${data.trackReferences.length})`, data);
151 return data;
152 }),
153 startWith(getTrackReferences(room, sources, onlySubscribedTracks)),
154 );
155
156 return observable;
157}
158
159export function participantTracksObservable(
160 participant: Participant,
161 trackIdentifier: ParticipantTrackIdentifier,
162): Observable<TrackReference[]> {
163 const observable = observeParticipantEvents(participant, ...participantTrackEvents).pipe(
164 map((participant) => {
165 const data = getParticipantTrackRefs(participant, trackIdentifier);
166 log.debug(`TrackReference[] was updated. (length ${data.length})`, data);
167 return data;
168 }),
169 startWith(getParticipantTrackRefs(participant, trackIdentifier)),
170 );
171
172 return observable;
173}
174
175export function trackEventSelector<T extends TrackEvent>(
176 publication: TrackPublication | Track,
177 event: T,
178) {
179 const observable = new Observable<
180 Parameters<PublicationEventCallbacks[Extract<T, keyof PublicationEventCallbacks>]>
181 >((subscribe) => {
182 const update = (
183 ...params: Parameters<PublicationEventCallbacks[Extract<T, keyof PublicationEventCallbacks>]>
184 ) => {
185 subscribe.next(params);
186 };
187 // @ts-expect-error not a perfect overlap between TrackEvent and keyof TrackEventCallbacks
188 publication.on(event, update);
189
190 const unsubscribe = () => {
191 // @ts-expect-error not a perfect overlap between TrackEvent and keyof TrackEventCallbacks
192 publication.off(event, update);
193 };
194 return unsubscribe;
195 });
196
197 return observable;
198}
199
200export function trackTranscriptionObserver(publication: TrackPublication) {
201 return trackEventSelector(publication, TrackEvent.TranscriptionReceived);
202}
203
204export function trackSyncTimeObserver(track: Track) {
205 return trackEventSelector(track, TrackEvent.TimeSyncUpdate).pipe(
206 map(([timeUpdate]) => timeUpdate),
207 ) as unknown as Observable<{ timestamp: number; rtpTimestamp?: number }>;
208}
Added view/app/src/vendor/livekit/core/persistent-storage/index.ts view/app/src/vendor/livekit/core/persistent-storage/index.ts +6 −0 View view/app/src/vendor/livekit/core/persistent-storage/index.ts
@@ -0,0 +1,6 @@
1export {
2 saveUserChoices,
3 loadUserChoices,
4 type LocalUserChoices,
5 defaultUserChoices,
6} from './user-choices';
Added view/app/src/vendor/livekit/core/persistent-storage/local-storage-helpers.ts view/app/src/vendor/livekit/core/persistent-storage/local-storage-helpers.ts +64 −0 View view/app/src/vendor/livekit/core/persistent-storage/local-storage-helpers.ts
@@ -0,0 +1,64 @@
1import { log } from '../logger';
2
3type JsonPrimitive = string | number | boolean | null;
4type JsonArray = JsonValue[];
5type JsonObject = { [key: string]: JsonValue };
6type JsonValue = JsonPrimitive | JsonArray | JsonObject;
7
8/**
9 * Persists a serializable object to local storage associated with the specified key.
10 * @internal
11 */
12function saveToLocalStorage<T extends JsonValue>(key: string, value: T): void {
13 if (typeof localStorage === 'undefined') {
14 log.error('Local storage is not available.');
15 return;
16 }
17
18 try {
19 if (value) {
20 const nonEmptySettings = Object.fromEntries(
21 Object.entries(value).filter(([, value]) => value !== ''),
22 );
23 localStorage.setItem(key, JSON.stringify(nonEmptySettings));
24 }
25 } catch (error) {
26 log.error(`Error setting item to local storage: ${error}`);
27 }
28}
29
30/**
31 * Retrieves a serializable object from local storage by its key.
32 * @internal
33 */
34function loadFromLocalStorage<T extends JsonValue>(key: string): T | undefined {
35 if (typeof localStorage === 'undefined') {
36 log.error('Local storage is not available.');
37 return undefined;
38 }
39
40 try {
41 const item = localStorage.getItem(key);
42 if (!item) {
43 log.warn(`Item with key ${key} does not exist in local storage.`);
44 return undefined;
45 }
46 return JSON.parse(item);
47 } catch (error) {
48 log.error(`Error getting item from local storage: ${error}`);
49 return undefined;
50 }
51}
52
53/**
54 * Generate a pair of functions to load and save a value of type T to local storage.
55 * @internal
56 */
57export function createLocalStorageInterface<T extends JsonValue>(
58 key: string,
59): { load: () => T | undefined; save: (value: T) => void } {
60 return {
61 load: () => loadFromLocalStorage<T>(key),
62 save: (value: T) => saveToLocalStorage<T>(key, value),
63 };
64}
Added view/app/src/vendor/livekit/core/persistent-storage/user-choices.ts view/app/src/vendor/livekit/core/persistent-storage/user-choices.ts +103 −0 View view/app/src/vendor/livekit/core/persistent-storage/user-choices.ts
@@ -0,0 +1,103 @@
1import { cssPrefix } from '../constants';
2import { createLocalStorageInterface } from './local-storage-helpers';
3
4const USER_CHOICES_KEY = `${cssPrefix}-user-choices` as const;
5
6/**
7 * @public
8 * Represents the user's choices for video and audio input devices,
9 * as well as their username.
10 */
11export type LocalUserChoices = {
12 /**
13 * Whether video input is enabled.
14 * @defaultValue `true`
15 */
16 videoEnabled: boolean;
17 /**
18 * Whether audio input is enabled.
19 * @defaultValue `true`
20 */
21 audioEnabled: boolean;
22 /**
23 * The device ID of the video input device to use.
24 * @defaultValue `''`
25 */
26 videoDeviceId: string;
27 /**
28 * The device ID of the audio input device to use.
29 * @defaultValue `''`
30 */
31 audioDeviceId: string;
32 /**
33 * The username to use.
34 * @defaultValue `''`
35 */
36 username: string;
37};
38
39export const defaultUserChoices: LocalUserChoices = {
40 videoEnabled: true,
41 audioEnabled: true,
42 videoDeviceId: 'default',
43 audioDeviceId: 'default',
44 username: '',
45} as const;
46
47/**
48 * The type of the object stored in local storage.
49 * @remarks
50 * TODO: Replace this type with `LocalUserChoices` after removing the deprecated properties from `LocalUserChoices`.
51 * @internal
52 */
53type TempStorageType = Omit<LocalUserChoices, 'e2ee' | 'sharedPassphrase'>;
54const { load, save } = createLocalStorageInterface<TempStorageType>(USER_CHOICES_KEY);
55
56/**
57 * Saves user choices to local storage.
58 * @alpha
59 */
60export function saveUserChoices(
61 userChoices: LocalUserChoices,
62 /**
63 * Whether to prevent saving user choices to local storage.
64 */
65 preventSave: boolean = false,
66): void {
67 if (preventSave === true) {
68 return;
69 }
70 save(userChoices);
71}
72
73/**
74 * Reads the user choices from local storage, or returns the default settings if none are found.
75 * @remarks
76 * The deprecated parameters `e2ee` and `sharedPassphrase` are not read from local storage
77 * and always return the value from the passed `defaults` or internal defaults.
78 * @alpha
79 */
80export function loadUserChoices(
81 defaults?: Partial<LocalUserChoices>,
82 /**
83 * Whether to prevent loading from local storage and return default values instead.
84 * @defaultValue false
85 */
86 preventLoad: boolean = false,
87): LocalUserChoices {
88 const fallback: LocalUserChoices = {
89 videoEnabled: defaults?.videoEnabled ?? defaultUserChoices.videoEnabled,
90 audioEnabled: defaults?.audioEnabled ?? defaultUserChoices.audioEnabled,
91 videoDeviceId: defaults?.videoDeviceId ?? defaultUserChoices.videoDeviceId,
92 audioDeviceId: defaults?.audioDeviceId ?? defaultUserChoices.audioDeviceId,
93 username: defaults?.username ?? defaultUserChoices.username,
94 };
95
96 if (preventLoad) {
97 return fallback;
98 } else {
99 const maybeLoadedObject = load();
100 const result = { ...fallback, ...(maybeLoadedObject ?? {}) };
101 return result;
102 }
103}
Added view/app/src/vendor/livekit/core/sorting/base-sort-functions.ts view/app/src/vendor/livekit/core/sorting/base-sort-functions.ts +101 −0 View view/app/src/vendor/livekit/core/sorting/base-sort-functions.ts
@@ -0,0 +1,101 @@
1import type { Participant } from 'livekit-client';
2import { Track } from 'livekit-client';
3import type { TrackReferenceOrPlaceholder } from '../track-reference';
4import { getTrackReferenceSource, isTrackReference } from '../track-reference';
5
6export function sortParticipantsByAudioLevel(
7 a: Pick<Participant, 'audioLevel'>,
8 b: Pick<Participant, 'audioLevel'>,
9): number {
10 return b.audioLevel - a.audioLevel;
11}
12
13export function sortParticipantsByIsSpeaking(
14 a: Pick<Participant, 'isSpeaking'>,
15 b: Pick<Participant, 'isSpeaking'>,
16): number {
17 if (a.isSpeaking === b.isSpeaking) {
18 return 0;
19 } else {
20 return a.isSpeaking ? -1 : 1;
21 }
22}
23
24export function sortParticipantsByLastSpokenAT(
25 a: Pick<Participant, 'lastSpokeAt'>,
26 b: Pick<Participant, 'lastSpokeAt'>,
27): number {
28 if (a.lastSpokeAt !== undefined || b.lastSpokeAt !== undefined) {
29 return (b.lastSpokeAt?.getTime() ?? 0) - (a.lastSpokeAt?.getTime() ?? 0);
30 } else {
31 return 0;
32 }
33}
34
35export function sortParticipantsByJoinedAt(
36 a: Pick<Participant, 'joinedAt'>,
37 b: Pick<Participant, 'joinedAt'>,
38) {
39 return (a.joinedAt?.getTime() ?? 0) - (b.joinedAt?.getTime() ?? 0);
40}
41
42export function sortTrackReferencesByType(
43 a: TrackReferenceOrPlaceholder,
44 b: TrackReferenceOrPlaceholder,
45) {
46 if (isTrackReference(a)) {
47 if (isTrackReference(b)) {
48 return 0;
49 } else {
50 return -1;
51 }
52 } else if (isTrackReference(b)) {
53 return 1;
54 } else {
55 return 0;
56 }
57}
58
59/** TrackReference with screen share source goes first. */
60export function sortTrackReferencesByScreenShare(
61 a: TrackReferenceOrPlaceholder,
62 b: TrackReferenceOrPlaceholder,
63): number {
64 const sourceA = getTrackReferenceSource(a);
65 const sourceB = getTrackReferenceSource(b);
66
67 if (sourceA === sourceB) {
68 if (sourceA === Track.Source.ScreenShare) {
69 if (a.participant.isLocal === b.participant.isLocal) {
70 return 0;
71 } else {
72 return a.participant.isLocal ? 1 : -1;
73 }
74 }
75 return 0;
76 } else if (sourceA === Track.Source.ScreenShare) {
77 return -1;
78 } else if (sourceB === Track.Source.ScreenShare) {
79 return 1;
80 } else {
81 return 0;
82 }
83}
84
85export function sortTrackRefsByIsCameraEnabled(
86 a: { participant: { isCameraEnabled: boolean } },
87 b: { participant: { isCameraEnabled: boolean } },
88) {
89 const aVideo = a.participant.isCameraEnabled;
90 const bVideo = b.participant.isCameraEnabled;
91
92 if (aVideo !== bVideo) {
93 if (aVideo) {
94 return -1;
95 } else {
96 return 1;
97 }
98 } else {
99 return 0;
100 }
101}
Added view/app/src/vendor/livekit/core/sorting/index.ts view/app/src/vendor/livekit/core/sorting/index.ts +3 −0 View view/app/src/vendor/livekit/core/sorting/index.ts
@@ -0,0 +1,3 @@
1export { sortTrackReferences } from './sort-track-bundles';
2export { sortParticipants } from './sort-participants';
3export { updatePages } from './tile-array-update';
Added view/app/src/vendor/livekit/core/sorting/sort-participants.ts view/app/src/vendor/livekit/core/sorting/sort-participants.ts +63 −0 View view/app/src/vendor/livekit/core/sorting/sort-participants.ts
@@ -0,0 +1,63 @@
1import type { Participant } from 'livekit-client';
2import { LocalParticipant } from 'livekit-client';
3import {
4 sortParticipantsByAudioLevel,
5 sortParticipantsByIsSpeaking,
6 sortParticipantsByJoinedAt,
7 sortParticipantsByLastSpokenAT,
8} from './base-sort-functions';
9
10/**
11 * Default sort for participants, it'll order participants by:
12 * 1. local participant
13 * 2. dominant speaker (speaker with the loudest audio level)
14 * 3. other speakers that are recently active
15 * 4. participants with video on
16 * 5. by joinedAt
17 */
18export function sortParticipants(participants: Participant[]): Participant[] {
19 const sortedParticipants = [...participants];
20 sortedParticipants.sort((a, b) => {
21 // loudest speaker first
22 if (a.isSpeaking && b.isSpeaking) {
23 return sortParticipantsByAudioLevel(a, b);
24 }
25
26 // speaker goes first
27 if (a.isSpeaking !== b.isSpeaking) {
28 return sortParticipantsByIsSpeaking(a, b);
29 }
30
31 // last active speaker first
32 if (a.lastSpokeAt !== b.lastSpokeAt) {
33 return sortParticipantsByLastSpokenAT(a, b);
34 }
35
36 // video on
37 const aVideo = a.videoTrackPublications.size > 0;
38 const bVideo = b.videoTrackPublications.size > 0;
39 if (aVideo !== bVideo) {
40 if (aVideo) {
41 return -1;
42 } else {
43 return 1;
44 }
45 }
46
47 // joinedAt
48 return sortParticipantsByJoinedAt(a, b);
49 });
50 const localParticipant = sortedParticipants.find((p) => p.isLocal) as LocalParticipant;
51 if (localParticipant) {
52 const localIdx = sortedParticipants.indexOf(localParticipant);
53 if (localIdx >= 0) {
54 sortedParticipants.splice(localIdx, 1);
55 if (sortedParticipants.length > 0) {
56 sortedParticipants.splice(0, 0, localParticipant);
57 } else {
58 sortedParticipants.push(localParticipant);
59 }
60 }
61 }
62 return sortedParticipants;
63}
Added view/app/src/vendor/livekit/core/sorting/sort-track-bundles.ts view/app/src/vendor/livekit/core/sorting/sort-track-bundles.ts +122 −0 View view/app/src/vendor/livekit/core/sorting/sort-track-bundles.ts
@@ -0,0 +1,122 @@
1import { Track } from 'livekit-client';
2import type { TrackReferenceOrPlaceholder } from '../track-reference';
3import { isTrackReference } from '../track-reference';
4import {
5 sortParticipantsByAudioLevel,
6 sortParticipantsByIsSpeaking,
7 sortParticipantsByJoinedAt,
8 sortParticipantsByLastSpokenAT,
9 sortTrackReferencesByType,
10 sortTrackRefsByIsCameraEnabled,
11} from './base-sort-functions';
12
13/**
14 * Default sort for `TrackReferenceOrPlaceholder`, it'll order participants by:
15 * 1. local camera track (publication.isLocal)
16 * 2. remote screen_share track
17 * 3. local screen_share track
18 * 4. remote dominant speaker camera track (sorted by speaker with the loudest audio level)
19 * 5. other remote speakers that are recently active
20 * 6. remote unmuted camera tracks
21 * 7. remote tracks sorted by joinedAt
22 */
23export function sortTrackReferences(
24 tracks: TrackReferenceOrPlaceholder[],
25): TrackReferenceOrPlaceholder[] {
26 const localTracks: TrackReferenceOrPlaceholder[] = [];
27 const screenShareTracks: TrackReferenceOrPlaceholder[] = [];
28 const cameraTracks: TrackReferenceOrPlaceholder[] = [];
29 const undefinedTracks: TrackReferenceOrPlaceholder[] = [];
30
31 tracks.forEach((trackRef) => {
32 if (trackRef.participant.isLocal && trackRef.source === Track.Source.Camera) {
33 localTracks.push(trackRef);
34 } else if (trackRef.source === Track.Source.ScreenShare) {
35 screenShareTracks.push(trackRef);
36 } else if (trackRef.source === Track.Source.Camera) {
37 cameraTracks.push(trackRef);
38 } else {
39 undefinedTracks.push(trackRef);
40 }
41 });
42
43 const sortedScreenShareTracks = sortScreenShareTracks(screenShareTracks);
44 const sortedCameraTracks = sortCameraTracks(cameraTracks);
45
46 return [...localTracks, ...sortedScreenShareTracks, ...sortedCameraTracks, ...undefinedTracks];
47}
48
49/**
50 * Sort an array of `TrackReference` screen shares.
51 * Main sorting order:
52 * 1. remote screen shares
53 * 2. local screen shares
54 * Secondary sorting by participant's joining time.
55 */
56function sortScreenShareTracks(
57 screenShareTracks: TrackReferenceOrPlaceholder[],
58): TrackReferenceOrPlaceholder[] {
59 const localScreenShares: TrackReferenceOrPlaceholder[] = [];
60 const remoteScreenShares: TrackReferenceOrPlaceholder[] = [];
61
62 screenShareTracks.forEach((trackRef) => {
63 if (trackRef.participant.isLocal) {
64 localScreenShares.push(trackRef);
65 } else {
66 remoteScreenShares.push(trackRef);
67 }
68 });
69
70 localScreenShares.sort((a, b) => sortParticipantsByJoinedAt(a.participant, b.participant));
71 remoteScreenShares.sort((a, b) => sortParticipantsByJoinedAt(a.participant, b.participant));
72
73 const sortedScreenShareTrackRefs = [...remoteScreenShares, ...localScreenShares];
74 return sortedScreenShareTrackRefs;
75}
76
77function sortCameraTracks(
78 cameraTrackReferences: TrackReferenceOrPlaceholder[],
79): TrackReferenceOrPlaceholder[] {
80 const localCameraTracks: TrackReferenceOrPlaceholder[] = [];
81 const remoteCameraTracks: TrackReferenceOrPlaceholder[] = [];
82
83 cameraTrackReferences.forEach((trackRef) => {
84 if (trackRef.participant.isLocal) {
85 localCameraTracks.push(trackRef);
86 } else {
87 remoteCameraTracks.push(trackRef);
88 }
89 });
90
91 remoteCameraTracks.sort((a, b) => {
92 // Participant with higher audio level goes first.
93 if (a.participant.isSpeaking && b.participant.isSpeaking) {
94 return sortParticipantsByAudioLevel(a.participant, b.participant);
95 }
96
97 // A speaking participant goes before one that is not speaking.
98 if (a.participant.isSpeaking !== b.participant.isSpeaking) {
99 return sortParticipantsByIsSpeaking(a.participant, b.participant);
100 }
101
102 // A participant that spoke recently goes before a participant that spoke a while back.
103 if (a.participant.lastSpokeAt !== b.participant.lastSpokeAt) {
104 return sortParticipantsByLastSpokenAT(a.participant, b.participant);
105 }
106
107 // TrackReference before TrackReferencePlaceholder
108 if (isTrackReference(a) !== isTrackReference(b)) {
109 return sortTrackReferencesByType(a, b);
110 }
111
112 // Tiles with video on before tiles with muted video track.
113 if (a.participant.isCameraEnabled !== b.participant.isCameraEnabled) {
114 return sortTrackRefsByIsCameraEnabled(a, b);
115 }
116
117 // A participant that joined a long time ago goes before one that joined recently.
118 return sortParticipantsByJoinedAt(a.participant, b.participant);
119 });
120
121 return [...localCameraTracks, ...remoteCameraTracks];
122}
Added view/app/src/vendor/livekit/core/sorting/tile-array-update.ts view/app/src/vendor/livekit/core/sorting/tile-array-update.ts +169 −0 View view/app/src/vendor/livekit/core/sorting/tile-array-update.ts
@@ -0,0 +1,169 @@
1import { differenceBy, chunk, zip } from '../helper/array-helper';
2import { log } from '../logger';
3import type { TrackReferenceOrPlaceholder } from '../track-reference';
4import {
5 getTrackReferenceId,
6 isPlaceholderReplacement,
7 isTrackReference,
8 isTrackReferencePlaceholder,
9} from '../track-reference';
10import { flatTrackReferenceArray } from '../track-reference/test-utils';
11
12type VisualChanges<T> = {
13 dropped: T[];
14 added: T[];
15};
16
17export type UpdatableItem = TrackReferenceOrPlaceholder | number;
18
19/** Check to see if anything visually changes on the page. */
20export function visualPageChange<T extends UpdatableItem>(state: T[], next: T[]): VisualChanges<T> {
21 return {
22 dropped: differenceBy(state, next, getTrackReferenceId),
23 added: differenceBy(next, state, getTrackReferenceId),
24 };
25}
26
27function listNeedsUpdating<T>(changes: VisualChanges<T>): boolean {
28 return changes.added.length !== 0 || changes.dropped.length !== 0;
29}
30
31export function findIndex<T extends UpdatableItem>(
32 trackReference: T,
33 trackReferences: T[],
34): number {
35 const indexToReplace = trackReferences.findIndex(
36 (trackReference_) =>
37 getTrackReferenceId(trackReference_) === getTrackReferenceId(trackReference),
38 );
39 if (indexToReplace === -1) {
40 throw new Error(
41 `Element not part of the array: ${getTrackReferenceId(
42 trackReference,
43 )} not in ${flatTrackReferenceArray(trackReferences)}`,
44 );
45 }
46 return indexToReplace;
47}
48
49/** Swap items in the complete list of all elements */
50export function swapItems<T extends UpdatableItem>(
51 moveForward: T,
52 moveBack: T,
53 trackReferences: T[],
54): T[] {
55 const indexToReplace = findIndex(moveForward, trackReferences);
56 const indexReplaceWith = findIndex(moveBack, trackReferences);
57
58 trackReferences.splice(indexToReplace, 1, moveBack);
59 trackReferences.splice(indexReplaceWith, 1, moveForward);
60
61 return trackReferences;
62}
63
64export function dropItem<T extends UpdatableItem>(itemToDrop: T, list: T[]): T[] {
65 const indexOfElementToDrop = findIndex(itemToDrop, list);
66 // const indexOfElementToDrop = list.findIndex((item) => item === itemToDrop, list);
67 list.splice(indexOfElementToDrop, 1);
68 return list;
69}
70
71function addItem<T extends UpdatableItem>(itemToAdd: T, list: T[]): T[] {
72 return [...list, itemToAdd];
73}
74
75export function divideIntoPages<T>(list: T[], maxElementsOnPage: number): Array<T[]> {
76 const pages = chunk(list, maxElementsOnPage);
77 return pages;
78}
79
80/** Divide the list of elements into pages and and check if pages need updating. */
81export function updatePages<T extends UpdatableItem>(
82 currentList: T[],
83 nextList: T[],
84 maxItemsOnPage: number,
85): T[] {
86 let updatedList: T[] = refreshList(currentList, nextList);
87
88 if (updatedList.length < nextList.length) {
89 // Items got added: Find newly added items and add them to the end of the list.
90 const addedItems = differenceBy(nextList, updatedList, getTrackReferenceId);
91 updatedList = [...updatedList, ...addedItems];
92 }
93 const currentPages = divideIntoPages(updatedList, maxItemsOnPage);
94 const nextPages = divideIntoPages(nextList, maxItemsOnPage);
95
96 zip(currentPages, nextPages).forEach(([currentPage, nextPage], pageIndex) => {
97 if (currentPage && nextPage) {
98 // 1) Identify missing tile.
99 const updatedPage = divideIntoPages(updatedList, maxItemsOnPage)[pageIndex];
100 const changes = visualPageChange(updatedPage, nextPage);
101
102 if (listNeedsUpdating(changes)) {
103 log.debug(
104 `Detected visual changes on page: ${pageIndex}, current: ${flatTrackReferenceArray(
105 currentPage,
106 )}, next: ${flatTrackReferenceArray(nextPage)}`,
107 { changes },
108 );
109 // ## Swap Items
110 if (changes.added.length === changes.dropped.length) {
111 zip(changes.added, changes.dropped).forEach(([added, dropped]) => {
112 if (added && dropped) {
113 updatedList = swapItems<T>(added, dropped, updatedList);
114 } else {
115 throw new Error(
116 `For a swap action we need a addition and a removal one is missing: ${added}, ${dropped}`,
117 );
118 }
119 });
120 }
121 // ## Handle Drop Items
122 if (changes.added.length === 0 && changes.dropped.length > 0) {
123 changes.dropped.forEach((item) => {
124 updatedList = dropItem<T>(item, updatedList);
125 });
126 }
127 // ## Handle Item added
128 if (changes.added.length > 0 && changes.dropped.length === 0) {
129 changes.added.forEach((item) => {
130 updatedList = addItem<T>(item, updatedList);
131 });
132 }
133 }
134 }
135 });
136
137 if (updatedList.length > nextList.length) {
138 // Items got removed: Find items that got completely removed from the list.
139 const missingItems = differenceBy(updatedList, nextList, getTrackReferenceId);
140 updatedList = updatedList.filter(
141 (item) => !missingItems.map(getTrackReferenceId).includes(getTrackReferenceId(item)),
142 );
143 }
144
145 return updatedList;
146}
147
148/**
149 * Update the current list with the items from the next list whenever the item ids are the same
150 * or the current item is a placeholder and we find a track reference in the next list
151 * to replace the placeholder with.
152 * @remarks
153 * This is needed because `TrackReference`s can change their internal state while keeping the same id.
154 */
155function refreshList<T extends UpdatableItem>(currentList: T[], nextList: T[]): T[] {
156 return currentList.map((currentItem) => {
157 const updateForCurrentItem = nextList.find(
158 (newItem_) =>
159 // If the IDs match or ..
160 getTrackReferenceId(currentItem) === getTrackReferenceId(newItem_) ||
161 // ... if the current item is a placeholder and the new item is the track reference can replace it.
162 (typeof currentItem !== 'number' &&
163 isTrackReferencePlaceholder(currentItem) &&
164 isTrackReference(newItem_) &&
165 isPlaceholderReplacement(currentItem, newItem_)),
166 );
167 return updateForCurrentItem ?? currentItem;
168 });
169}
Added view/app/src/vendor/livekit/core/styles-interface/class-prefixer.ts view/app/src/vendor/livekit/core/styles-interface/class-prefixer.ts +14 −0 View view/app/src/vendor/livekit/core/styles-interface/class-prefixer.ts
@@ -0,0 +1,14 @@
1import type { UnprefixedClassNames as ComponentNoPrefixClasses } from '@livekit/components-styles/dist/types_unprefixed/index.scss';
2import type { UnprefixedClassNames as PrefabNoPrefixClasses } from '@livekit/components-styles/dist/types_unprefixed/prefabs/index.scss';
3import { cssPrefix } from './../constants';
4
5type UnprefixedClassNames = ComponentNoPrefixClasses | PrefabNoPrefixClasses;
6
7/**
8 * This function is a type safe way to add a prefix to a HTML class attribute.
9 * Only classes defined in the styles module are valid, any other class produces a ts error.
10 * @internal
11 */
12export function prefixClass<T extends UnprefixedClassNames>(unprefixedClassName: T) {
13 return `${cssPrefix}-${unprefixedClassName}` as const;
14}
Added view/app/src/vendor/livekit/core/styles-interface/index.ts view/app/src/vendor/livekit/core/styles-interface/index.ts +1 −0 View view/app/src/vendor/livekit/core/styles-interface/index.ts
@@ -0,0 +1 @@
1export { prefixClass } from './class-prefixer';
Added view/app/src/vendor/livekit/core/track-reference/index.ts view/app/src/vendor/livekit/core/track-reference/index.ts +2 −0 View view/app/src/vendor/livekit/core/track-reference/index.ts
@@ -0,0 +1,2 @@
1export * from './track-reference.types';
2export * from './track-reference.utils';
Added view/app/src/vendor/livekit/core/track-reference/test-utils.ts view/app/src/vendor/livekit/core/track-reference/test-utils.ts +102 −0 View view/app/src/vendor/livekit/core/track-reference/test-utils.ts
@@ -0,0 +1,102 @@
1/**
2 * Internal test function.
3 *
4 * @internal
5 */
6
7import { Participant, RemoteTrackPublication, Track, TrackPublication } from 'livekit-client';
8import type { UpdatableItem } from '../sorting/tile-array-update';
9import type { TrackReference, TrackReferencePlaceholder } from './track-reference.types';
10import { getTrackReferenceId } from './track-reference.utils';
11import { TrackInfo } from '@livekit/protocol';
12
13// Test function:
14export const mockTrackReferencePlaceholder = (
15 id: string,
16 source: Track.Source,
17): TrackReferencePlaceholder => {
18 return { participant: new Participant(`${id}`, `${id}`), source };
19};
20
21export const mockTrackReferencePublished = (id: string, source: Track.Source): TrackReference => {
22 const kind = [Track.Source.Camera, Track.Source.ScreenShare].includes(source)
23 ? Track.Kind.Video
24 : Track.Kind.Audio;
25 const trackInfo = new TrackInfo({
26 sid: `${id}`,
27 name: `${id}`,
28 muted: false,
29 });
30 return {
31 participant: new Participant(`${id}`, `${id}`),
32 publication: new RemoteTrackPublication(kind, trackInfo, true),
33 source: source,
34 };
35};
36
37type mockTrackReferenceSubscribedOptions = {
38 mockPublication?: boolean;
39 mockParticipant?: boolean;
40 mockIsLocal?: boolean;
41};
42
43export const mockTrackReferenceSubscribed = (
44 id: string,
45 source: Track.Source,
46 options: mockTrackReferenceSubscribedOptions = {},
47): TrackReference => {
48 const kind = [Track.Source.Camera, Track.Source.ScreenShare].includes(source)
49 ? Track.Kind.Video
50 : Track.Kind.Audio;
51 const trackInfo = new TrackInfo({
52 sid: `${id}`,
53 name: `${id}`,
54 muted: false,
55 });
56 const publication = new RemoteTrackPublication(kind, trackInfo, true);
57 // @ts-expect-error
58 publication.track = {};
59 return {
60 participant: options.mockParticipant
61 ? (mockParticipant(id, options.mockIsLocal ?? false) as Participant)
62 : new Participant(`${id}`, `${id}`),
63 publication: options.mockPublication
64 ? (mockTrackPublication(`publicationId(${id})`, kind, source) as TrackPublication)
65 : publication,
66 source,
67 };
68};
69
70const mockTrackPublication = (
71 id: string,
72 kind: Track.Kind,
73 source: Track.Source,
74): Pick<TrackPublication, 'kind' | 'trackSid' | 'trackName' | 'source'> => {
75 return {
76 kind,
77 trackSid: id,
78 trackName: `name_${id}`,
79 source: source,
80 };
81};
82
83function mockParticipant(
84 id: string,
85 isLocal: boolean,
86): Pick<Participant, 'sid' | 'identity' | 'isLocal'> {
87 return {
88 sid: `${id}_sid`,
89 identity: `${id}`,
90 isLocal: isLocal,
91 };
92}
93
94export function flatTrackReferenceArray<T extends UpdatableItem>(list: T[]): string[] {
95 return list.map((item) => {
96 if (typeof item === 'string' || typeof item === 'number') {
97 return `${item}`;
98 } else {
99 return getTrackReferenceId(item);
100 }
101 });
102}
Added view/app/src/vendor/livekit/core/track-reference/track-reference.types.ts view/app/src/vendor/livekit/core/track-reference/track-reference.types.ts +73 −0 View view/app/src/vendor/livekit/core/track-reference/track-reference.types.ts
@@ -0,0 +1,73 @@
1/**
2 * The TrackReference type is a logical grouping of participant publication and/or subscribed track.
3 *
4 */
5
6import type { Participant, Track, TrackPublication } from 'livekit-client';
7// ## TrackReference Types
8
9/** @public */
10export type TrackReferencePlaceholder = {
11 participant: Participant;
12 publication?: never;
13 source: Track.Source;
14};
15
16/** @public */
17export type TrackReference = {
18 participant: Participant;
19 publication: TrackPublication;
20 source: Track.Source;
21};
22
23/** @public */
24export type TrackReferenceOrPlaceholder = TrackReference | TrackReferencePlaceholder;
25
26// ### TrackReference Type Predicates
27/** @internal */
28export function isTrackReference(trackReference: unknown): trackReference is TrackReference {
29 if (typeof trackReference === 'undefined') {
30 return false;
31 }
32 return (
33 isTrackReferenceSubscribed(trackReference as TrackReference) ||
34 isTrackReferencePublished(trackReference as TrackReference)
35 );
36}
37
38function isTrackReferenceSubscribed(trackReference?: TrackReferenceOrPlaceholder): boolean {
39 if (!trackReference) {
40 return false;
41 }
42 return (
43 trackReference.hasOwnProperty('participant') &&
44 trackReference.hasOwnProperty('source') &&
45 trackReference.hasOwnProperty('track') &&
46 typeof trackReference.publication?.track !== 'undefined'
47 );
48}
49
50function isTrackReferencePublished(trackReference?: TrackReferenceOrPlaceholder): boolean {
51 if (!trackReference) {
52 return false;
53 }
54 return (
55 trackReference.hasOwnProperty('participant') &&
56 trackReference.hasOwnProperty('source') &&
57 trackReference.hasOwnProperty('publication') &&
58 typeof trackReference.publication !== 'undefined'
59 );
60}
61
62export function isTrackReferencePlaceholder(
63 trackReference?: TrackReferenceOrPlaceholder,
64): trackReference is TrackReferencePlaceholder {
65 if (!trackReference) {
66 return false;
67 }
68 return (
69 trackReference.hasOwnProperty('participant') &&
70 trackReference.hasOwnProperty('source') &&
71 typeof trackReference.publication === 'undefined'
72 );
73}
Added view/app/src/vendor/livekit/core/track-reference/track-reference.utils.ts view/app/src/vendor/livekit/core/track-reference/track-reference.utils.ts +97 −0 View view/app/src/vendor/livekit/core/track-reference/track-reference.utils.ts
@@ -0,0 +1,97 @@
1import type { Track } from 'livekit-client';
2import type { PinState } from '../types';
3import type { TrackReferenceOrPlaceholder } from './track-reference.types';
4import { isTrackReference, isTrackReferencePlaceholder } from './track-reference.types';
5
6/**
7 * Returns a id to identify the `TrackReference` or `TrackReferencePlaceholder` based on
8 * participant, track source and trackSid.
9 * @remarks
10 * The id pattern is: `${participantIdentity}_${trackSource}_${trackSid}` for `TrackReference`
11 * and `${participantIdentity}_${trackSource}_placeholder` for `TrackReferencePlaceholder`.
12 */
13export function getTrackReferenceId(trackReference: TrackReferenceOrPlaceholder | number) {
14 if (typeof trackReference === 'string' || typeof trackReference === 'number') {
15 return `${trackReference}`;
16 } else if (isTrackReferencePlaceholder(trackReference)) {
17 return `${trackReference.participant.identity}_${trackReference.source}_placeholder`;
18 } else if (isTrackReference(trackReference)) {
19 return `${trackReference.participant.identity}_${trackReference.publication.source}_${trackReference.publication.trackSid}`;
20 } else {
21 throw new Error(`Can't generate a id for the given track reference: ${trackReference}`);
22 }
23}
24
25export type TrackReferenceId = ReturnType<typeof getTrackReferenceId>;
26
27/** Returns the Source of the TrackReference. */
28export function getTrackReferenceSource(trackReference: TrackReferenceOrPlaceholder): Track.Source {
29 if (isTrackReference(trackReference)) {
30 return trackReference.publication.source;
31 } else {
32 return trackReference.source;
33 }
34}
35
36export function isEqualTrackRef(
37 a?: TrackReferenceOrPlaceholder,
38 b?: TrackReferenceOrPlaceholder,
39): boolean {
40 if (a === undefined || b === undefined) {
41 return false;
42 }
43 if (isTrackReference(a) && isTrackReference(b)) {
44 return a.publication.trackSid === b.publication.trackSid;
45 } else {
46 return getTrackReferenceId(a) === getTrackReferenceId(b);
47 }
48}
49
50/**
51 * Check if the `TrackReference` is pinned.
52 */
53export function isTrackReferencePinned(
54 trackReference: TrackReferenceOrPlaceholder,
55 pinState: PinState | undefined,
56): boolean {
57 if (typeof pinState === 'undefined') {
58 return false;
59 }
60 if (isTrackReference(trackReference)) {
61 return pinState.some(
62 (pinnedTrackReference) =>
63 pinnedTrackReference.participant.identity === trackReference.participant.identity &&
64 isTrackReference(pinnedTrackReference) &&
65 pinnedTrackReference.publication.trackSid === trackReference.publication.trackSid,
66 );
67 } else if (isTrackReferencePlaceholder(trackReference)) {
68 return pinState.some(
69 (pinnedTrackReference) =>
70 pinnedTrackReference.participant.identity === trackReference.participant.identity &&
71 isTrackReferencePlaceholder(pinnedTrackReference) &&
72 pinnedTrackReference.source === trackReference.source,
73 );
74 } else {
75 return false;
76 }
77}
78
79/**
80 * Check if the current `currentTrackRef` is the placeholder for next `nextTrackRef`.
81 * Based on the participant identity and the source.
82 * @internal
83 */
84export function isPlaceholderReplacement(
85 currentTrackRef: TrackReferenceOrPlaceholder,
86 nextTrackRef: TrackReferenceOrPlaceholder,
87) {
88 // if (typeof nextTrackRef === 'number' || typeof currentTrackRef === 'number') {
89 // return false;
90 // }
91 return (
92 isTrackReferencePlaceholder(currentTrackRef) &&
93 isTrackReference(nextTrackRef) &&
94 nextTrackRef.participant.identity === currentTrackRef.participant.identity &&
95 nextTrackRef.source === currentTrackRef.source
96 );
97}
Added view/app/src/vendor/livekit/core/constants.ts view/app/src/vendor/livekit/core/constants.ts +1 −0 View view/app/src/vendor/livekit/core/constants.ts
@@ -0,0 +1 @@
1export const cssPrefix = 'lk';
Added view/app/src/vendor/livekit/core/index.ts view/app/src/vendor/livekit/core/index.ts +34 −0 View view/app/src/vendor/livekit/core/index.ts
@@ -0,0 +1,34 @@
1export * from './constants';
2export * from './utils';
3export * from './helper';
4export * from './types';
5export * from './sorting';
6export * from './track-reference';
7export * from './messages';
8
9export * from './components/mediaToggle';
10export * from './components/mediaDeviceSelect';
11export * from './components/disconnectButton';
12export * from './components/mediaTrack';
13export * from './components/connectionQualityIndicator';
14export * from './components/trackMutedIndicator';
15export * from './components/participantName';
16export * from './components/mediaTrack';
17export * from './components/participantTile';
18export * from './components/chat';
19export * from './components/startAudio';
20export * from './components/startVideo';
21export * from './components/chatToggle';
22export * from './components/focusToggle';
23export * from './components/clearPinButton';
24export * from './components/room';
25
26export * from './observables/room';
27export * from './observables/participant';
28export * from './observables/track';
29export * from './observables/dataChannel';
30export * from './observables/dom-event';
31
32export * from './persistent-storage';
33
34export { log, setLogLevel, setLogExtension } from './logger';
Added view/app/src/vendor/livekit/core/logger.ts view/app/src/vendor/livekit/core/logger.ts +55 −0 View view/app/src/vendor/livekit/core/logger.ts
@@ -0,0 +1,55 @@
1import {
2 setLogLevel as setClientSdkLogLevel,
3 setLogExtension as setClientSdkLogExtension,
4 LogLevel as LogLevelEnum,
5} from 'livekit-client';
6import loglevel from 'loglevel';
7
8export const log = loglevel.getLogger('lk-components-js');
9log.setDefaultLevel('WARN');
10
11type LogLevel = Parameters<typeof setClientSdkLogLevel>[0];
12type SetLogLevelOptions = {
13 liveKitClientLogLevel?: LogLevel;
14};
15
16/**
17 * Set the log level for both the `@livekit/components-react` package and the `@livekit-client` package.
18 * To set the `@livekit-client` log independently, use the `liveKitClientLogLevel` prop on the `options` object.
19 * @public
20 */
21export function setLogLevel(level: LogLevel, options: SetLogLevelOptions = {}): void {
22 log.setLevel(level);
23 setClientSdkLogLevel(options.liveKitClientLogLevel ?? level);
24}
25
26type LogExtension = (level: LogLevel, msg: string, context?: object) => void;
27type SetLogExtensionOptions = {
28 liveKitClientLogExtension?: LogExtension;
29};
30
31/**
32 * Set the log extension for both the `@livekit/components-react` package and the `@livekit-client` package.
33 * To set the `@livekit-client` log extension, use the `liveKitClientLogExtension` prop on the `options` object.
34 * @public
35 */
36export function setLogExtension(extension: LogExtension, options: SetLogExtensionOptions = {}) {
37 const originalFactory = log.methodFactory;
38
39 log.methodFactory = (methodName, configLevel, loggerName) => {
40 const rawMethod = originalFactory(methodName, configLevel, loggerName);
41
42 const logLevel = LogLevelEnum[methodName];
43 const needLog = logLevel >= configLevel && logLevel < LogLevelEnum.silent;
44
45 return (msg, context?: [msg: string, context: object]) => {
46 if (context) rawMethod(msg, context);
47 else rawMethod(msg);
48 if (needLog) {
49 extension(logLevel, msg, context);
50 }
51 };
52 };
53 log.setLevel(log.getLevel()); // Be sure to call setLevel method in order to apply plugin
54 setClientSdkLogExtension(options.liveKitClientLogExtension ?? extension);
55}
Added view/app/src/vendor/livekit/core/types.ts view/app/src/vendor/livekit/core/types.ts +89 −0 View view/app/src/vendor/livekit/core/types.ts
@@ -0,0 +1,89 @@
1import type { Participant, ParticipantKind, Track, TrackPublication } from 'livekit-client';
2import type { TrackReference, TrackReferenceOrPlaceholder } from './track-reference';
3
4// ## PinState Type
5/** @public */
6export type PinState = TrackReferenceOrPlaceholder[];
7export const PIN_DEFAULT_STATE: PinState = [];
8
9// ## WidgetState Types
10/** @public */
11export type WidgetState = {
12 showChat: boolean;
13 unreadMessages: number;
14 showSettings?: boolean;
15};
16export const WIDGET_DEFAULT_STATE: WidgetState = {
17 showChat: false,
18 unreadMessages: 0,
19 showSettings: false,
20};
21
22// ## Track Source Types
23export type TrackSourceWithOptions = { source: Track.Source; withPlaceholder: boolean };
24
25export type SourcesArray = Track.Source[] | TrackSourceWithOptions[];
26
27// ### Track Source Type Predicates
28export function isSourceWitOptions(source: SourcesArray[number]): source is TrackSourceWithOptions {
29 return typeof source === 'object';
30}
31
32export function isSourcesWithOptions(sources: SourcesArray): sources is TrackSourceWithOptions[] {
33 return (
34 Array.isArray(sources) &&
35 (sources as TrackSourceWithOptions[]).filter(isSourceWitOptions).length > 0
36 );
37}
38
39// ## Loop Filter Types
40export type TrackReferenceFilter = Parameters<TrackReferenceOrPlaceholder[]['filter']>['0'];
41export type ParticipantFilter = Parameters<Participant[]['filter']>['0'];
42
43// ## Other Types
44/** @internal */
45export interface ParticipantClickEvent {
46 participant: Participant;
47 track?: TrackPublication;
48}
49
50export type TrackSource<T extends Track.Source> = RequireAtLeastOne<
51 { source: T; name: string; participant: Participant },
52 'name' | 'source'
53>;
54
55export type ParticipantTrackIdentifier = RequireAtLeastOne<
56 { sources: Track.Source[]; name: string; kind: Track.Kind },
57 'sources' | 'name' | 'kind'
58>;
59
60/**
61 * @beta
62 */
63export type ParticipantIdentifier = RequireAtLeastOne<
64 { kind: ParticipantKind; identity: string },
65 'identity' | 'kind'
66>;
67
68/**
69 * The TrackIdentifier type is used to select Tracks either based on
70 * - Track.Source and/or name of the track, e.g. `{source: Track.Source.Camera}` or `{name: "my-track"}`
71 * - TrackReference (participant and publication)
72 * @internal
73 */
74export type TrackIdentifier<T extends Track.Source = Track.Source> =
75 TrackSource<T> | TrackReference;
76
77// ## Util Types
78type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> &
79 {
80 [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>;
81 }[Keys];
82
83export type RequireOnlyOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> &
84 {
85 [K in Keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<Keys, K>, undefined>>;
86 }[Keys];
87
88export type AudioSource = Track.Source.Microphone | Track.Source.ScreenShareAudio;
89export type VideoSource = Track.Source.Camera | Track.Source.ScreenShare;
Added view/app/src/vendor/livekit/core/utils.ts view/app/src/vendor/livekit/core/utils.ts +74 −0 View view/app/src/vendor/livekit/core/utils.ts
@@ -0,0 +1,74 @@
1import type { Participant, TrackPublication } from 'livekit-client';
2
3import type { TrackReference } from './track-reference';
4import { isEqualTrackRef } from './track-reference';
5import type { PinState } from './types';
6
7export function isLocal(p: Participant) {
8 return p.isLocal;
9}
10
11export function isRemote(p: Participant) {
12 return !p.isLocal;
13}
14
15export const attachIfSubscribed = (
16 publication: TrackPublication | undefined,
17 element: HTMLMediaElement | null | undefined,
18) => {
19 if (!publication) return;
20 const { isSubscribed, track } = publication;
21 if (element && track) {
22 if (isSubscribed) {
23 track.attach(element);
24 } else {
25 track.detach(element);
26 }
27 }
28};
29
30/**
31 * Check if the participant track reference is pinned.
32 */
33export function isParticipantTrackReferencePinned(
34 trackRef: TrackReference,
35 pinState: PinState | undefined,
36): boolean {
37 if (pinState === undefined) {
38 return false;
39 }
40
41 return pinState.some((pinnedTrackRef) => isEqualTrackRef(pinnedTrackRef, trackRef));
42}
43
44/**
45 * Calculates the scrollbar width by creating two HTML elements
46 * and messaging the difference.
47 * @internal
48 */
49export function getScrollBarWidth() {
50 const inner = document.createElement('p');
51 inner.style.width = '100%';
52 inner.style.height = '200px';
53
54 const outer = document.createElement('div');
55 outer.style.position = 'absolute';
56 outer.style.top = '0px';
57 outer.style.left = '0px';
58 outer.style.visibility = 'hidden';
59 outer.style.width = '200px';
60 outer.style.height = '150px';
61 outer.style.overflow = 'hidden';
62 outer.appendChild(inner);
63
64 document.body.appendChild(outer);
65 const w1 = inner.offsetWidth;
66 outer.style.overflow = 'scroll';
67 let w2 = inner.offsetWidth;
68 if (w1 === w2) {
69 w2 = outer.clientWidth;
70 }
71 document.body.removeChild(outer);
72 const scrollBarWidth = w1 - w2;
73 return scrollBarWidth;
74}
Added view/app/src/vendor/livekit/react/assets/icons/CameraDisabledIcon.tsx view/app/src/vendor/livekit/react/assets/icons/CameraDisabledIcon.tsx +14 −0 View view/app/src/vendor/livekit/react/assets/icons/CameraDisabledIcon.tsx
@@ -0,0 +1,14 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgCameraDisabledIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="currentColor" {...props}>
10 <path d="M1.354.646a.5.5 0 1 0-.708.708l14 14a.5.5 0 0 0 .708-.708L11 10.293V4.5A1.5 1.5 0 0 0 9.5 3H3.707zM0 4.5a1.5 1.5 0 0 1 .943-1.393l9.532 9.533c-.262.224-.603.36-.975.36h-8A1.5 1.5 0 0 1 0 11.5z" />
11 <path d="m15.2 3.6-2.8 2.1a1 1 0 0 0-.4.8v3a1 1 0 0 0 .4.8l2.8 2.1a.5.5 0 0 0 .8-.4V4a.5.5 0 0 0-.8-.4" />
12 </svg>
13);
14export default SvgCameraDisabledIcon;
Added view/app/src/vendor/livekit/react/assets/icons/CameraIcon.tsx view/app/src/vendor/livekit/react/assets/icons/CameraIcon.tsx +13 −0 View view/app/src/vendor/livekit/react/assets/icons/CameraIcon.tsx
@@ -0,0 +1,13 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgCameraIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="currentColor" {...props}>
10 <path d="M0 4.5A1.5 1.5 0 0 1 1.5 3h8A1.5 1.5 0 0 1 11 4.5v7A1.5 1.5 0 0 1 9.5 13h-8A1.5 1.5 0 0 1 0 11.5zM15.2 3.6l-2.8 2.1a1 1 0 0 0-.4.8v3a1 1 0 0 0 .4.8l2.8 2.1a.5.5 0 0 0 .8-.4V4a.5.5 0 0 0-.8-.4" />
11 </svg>
12);
13export default SvgCameraIcon;
Added view/app/src/vendor/livekit/react/assets/icons/ChatCloseIcon.tsx view/app/src/vendor/livekit/react/assets/icons/ChatCloseIcon.tsx +16 −0 View view/app/src/vendor/livekit/react/assets/icons/ChatCloseIcon.tsx
@@ -0,0 +1,16 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgChatCloseIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} viewBox="0 0 24 24" {...props}>
10 <path
11 fill="#FFF"
12 d="M4.99 3.99a1 1 0 0 0-.697 1.717L10.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414L12 13.414l6.293 6.293a1 1 0 1 0 1.414-1.414L13.414 12l6.293-6.293a1 1 0 0 0-.727-1.717 1 1 0 0 0-.687.303L12 10.586 5.707 4.293a1 1 0 0 0-.717-.303"
13 />
14 </svg>
15);
16export default SvgChatCloseIcon;
Added view/app/src/vendor/livekit/react/assets/icons/ChatIcon.tsx view/app/src/vendor/livekit/react/assets/icons/ChatIcon.tsx +24 −0 View view/app/src/vendor/livekit/react/assets/icons/ChatIcon.tsx
@@ -0,0 +1,24 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgChatIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={18} fill="none" {...props}>
10 <path
11 fill="currentColor"
12 fillRule="evenodd"
13 d="M0 2.75A2.75 2.75 0 0 1 2.75 0h10.5A2.75 2.75 0 0 1 16 2.75v13.594a.75.75 0 0 1-1.234.572l-3.691-3.12a1.25 1.25 0 0 0-.807-.296H2.75A2.75 2.75 0 0 1 0 10.75zM2.75 1.5c-.69 0-1.25.56-1.25 1.25v8c0 .69.56 1.25 1.25 1.25h7.518c.65 0 1.279.23 1.775.65l2.457 2.077V2.75c0-.69-.56-1.25-1.25-1.25z"
14 clipRule="evenodd"
15 />
16 <path
17 fill="currentColor"
18 fillRule="evenodd"
19 d="M3 4.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5"
20 clipRule="evenodd"
21 />
22 </svg>
23);
24export default SvgChatIcon;
Added view/app/src/vendor/livekit/react/assets/icons/Chevron.tsx view/app/src/vendor/livekit/react/assets/icons/Chevron.tsx +18 −0 View view/app/src/vendor/livekit/react/assets/icons/Chevron.tsx
@@ -0,0 +1,18 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgChevron = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="none" {...props}>
10 <path
11 fill="currentcolor"
12 fillRule="evenodd"
13 d="M5.293 2.293a1 1 0 0 1 1.414 0l4.823 4.823a1.25 1.25 0 0 1 0 1.768l-4.823 4.823a1 1 0 0 1-1.414-1.414L9.586 8 5.293 3.707a1 1 0 0 1 0-1.414"
14 clipRule="evenodd"
15 />
16 </svg>
17);
18export default SvgChevron;
Added view/app/src/vendor/livekit/react/assets/icons/FocusToggleIcon.tsx view/app/src/vendor/livekit/react/assets/icons/FocusToggleIcon.tsx +15 −0 View view/app/src/vendor/livekit/react/assets/icons/FocusToggleIcon.tsx
@@ -0,0 +1,15 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgFocusToggleIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="none" {...props}>
10 <g stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}>
11 <path d="M10 1.75h4.25m0 0V6m0-4.25L9 7M6 14.25H1.75m0 0V10m0 4.25L7 9" />
12 </g>
13 </svg>
14);
15export default SvgFocusToggleIcon;
Added view/app/src/vendor/livekit/react/assets/icons/GearIcon.tsx view/app/src/vendor/livekit/react/assets/icons/GearIcon.tsx +18 −0 View view/app/src/vendor/livekit/react/assets/icons/GearIcon.tsx
@@ -0,0 +1,18 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgGearIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="none" {...props}>
10 <path
11 fill="currentcolor"
12 fillRule="evenodd"
13 d="M8.961.894C8.875-.298 7.125-.298 7.04.894c-.066.912-1.246 1.228-1.76.472-.67-.99-2.186-.115-1.664.96.399.824-.465 1.688-1.288 1.289-1.076-.522-1.95.994-.961 1.665.756.513.44 1.693-.472 1.759-1.192.086-1.192 1.836 0 1.922.912.066 1.228 1.246.472 1.76-.99.67-.115 2.186.96 1.664.824-.399 1.688.465 1.289 1.288-.522 1.076.994 1.95 1.665.961.513-.756 1.693-.44 1.759.472.086 1.192 1.836 1.192 1.922 0 .066-.912 1.246-1.228 1.76-.472.67.99 2.186.115 1.664-.96-.399-.824.465-1.687 1.288-1.289 1.076.522 1.95-.994.961-1.665-.756-.513-.44-1.693.472-1.759 1.192-.086 1.192-1.836 0-1.922-.912-.066-1.228-1.246-.472-1.76.99-.67.115-2.186-.96-1.664-.824.399-1.687-.465-1.289-1.288.522-1.076-.994-1.95-1.665-.961-.513.756-1.693.44-1.759-.472M8 13A5 5 0 1 0 8 3a5 5 0 0 0 0 10"
14 clipRule="evenodd"
15 />
16 </svg>
17);
18export default SvgGearIcon;
Added view/app/src/vendor/livekit/react/assets/icons/index.ts view/app/src/vendor/livekit/react/assets/icons/index.ts +19 −0 View view/app/src/vendor/livekit/react/assets/icons/index.ts
@@ -0,0 +1,19 @@
1export { default as CameraDisabledIcon } from './CameraDisabledIcon';
2export { default as CameraIcon } from './CameraIcon';
3export { default as ChatCloseIcon } from './ChatCloseIcon';
4export { default as ChatIcon } from './ChatIcon';
5export { default as Chevron } from './Chevron';
6export { default as FocusToggleIcon } from './FocusToggleIcon';
7export { default as GearIcon } from './GearIcon';
8export { default as LeaveIcon } from './LeaveIcon';
9export { default as LockLockedIcon } from './LockLockedIcon';
10export { default as MicDisabledIcon } from './MicDisabledIcon';
11export { default as MicIcon } from './MicIcon';
12export { default as QualityExcellentIcon } from './QualityExcellentIcon';
13export { default as QualityGoodIcon } from './QualityGoodIcon';
14export { default as QualityPoorIcon } from './QualityPoorIcon';
15export { default as QualityUnknownIcon } from './QualityUnknownIcon';
16export { default as ScreenShareIcon } from './ScreenShareIcon';
17export { default as ScreenShareStopIcon } from './ScreenShareStopIcon';
18export { default as SpinnerIcon } from './SpinnerIcon';
19export { default as UnfocusToggleIcon } from './UnfocusToggleIcon';
Added view/app/src/vendor/livekit/react/assets/icons/LeaveIcon.tsx view/app/src/vendor/livekit/react/assets/icons/LeaveIcon.tsx +24 −0 View view/app/src/vendor/livekit/react/assets/icons/LeaveIcon.tsx
@@ -0,0 +1,24 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgLeaveIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="none" {...props}>
10 <path
11 fill="currentColor"
12 fillRule="evenodd"
13 d="M2 2.75A2.75 2.75 0 0 1 4.75 0h6.5A2.75 2.75 0 0 1 14 2.75v10.5A2.75 2.75 0 0 1 11.25 16h-6.5A2.75 2.75 0 0 1 2 13.25v-.5a.75.75 0 0 1 1.5 0v.5c0 .69.56 1.25 1.25 1.25h6.5c.69 0 1.25-.56 1.25-1.25V2.75c0-.69-.56-1.25-1.25-1.25h-6.5c-.69 0-1.25.56-1.25 1.25v.5a.75.75 0 0 1-1.5 0z"
14 clipRule="evenodd"
15 />
16 <path
17 fill="currentColor"
18 fillRule="evenodd"
19 d="M8.78 7.47a.75.75 0 0 1 0 1.06l-2.25 2.25a.75.75 0 1 1-1.06-1.06l.97-.97H1.75a.75.75 0 0 1 0-1.5h4.69l-.97-.97a.75.75 0 0 1 1.06-1.06z"
20 clipRule="evenodd"
21 />
22 </svg>
23);
24export default SvgLeaveIcon;
Added view/app/src/vendor/livekit/react/assets/icons/LockLockedIcon.tsx view/app/src/vendor/livekit/react/assets/icons/LockLockedIcon.tsx +18 −0 View view/app/src/vendor/livekit/react/assets/icons/LockLockedIcon.tsx
@@ -0,0 +1,18 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgLockLockedIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="none" {...props}>
10 <path
11 fill="currentcolor"
12 fillRule="evenodd"
13 d="M4 6.104V4a4 4 0 1 1 8 0v2.104c1.154.326 2 1.387 2 2.646v4.5A2.75 2.75 0 0 1 11.25 16h-6.5A2.75 2.75 0 0 1 2 13.25v-4.5c0-1.259.846-2.32 2-2.646M5.5 4a2.5 2.5 0 0 1 5 0v2h-5z"
14 clipRule="evenodd"
15 />
16 </svg>
17);
18export default SvgLockLockedIcon;
Added view/app/src/vendor/livekit/react/assets/icons/MicDisabledIcon.tsx view/app/src/vendor/livekit/react/assets/icons/MicDisabledIcon.tsx +14 −0 View view/app/src/vendor/livekit/react/assets/icons/MicDisabledIcon.tsx
@@ -0,0 +1,14 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgMicDisabledIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="currentColor" {...props}>
10 <path d="M12.227 11.52a5.48 5.48 0 0 0 1.246-2.97.5.5 0 0 0-.995-.1 4.5 4.5 0 0 1-.962 2.359l-1.07-1.07C10.794 9.247 11 8.647 11 8V3a3 3 0 0 0-6 0v1.293L1.354.646a.5.5 0 1 0-.708.708l14 14a.5.5 0 0 0 .708-.708zM8 12.5c.683 0 1.33-.152 1.911-.425l.743.743c-.649.359-1.378.59-2.154.66V15h2a.5.5 0 0 1 0 1h-5a.5.5 0 0 1 0-1h2v-1.522a5.5 5.5 0 0 1-4.973-4.929.5.5 0 0 1 .995-.098A4.5 4.5 0 0 0 8 12.5" />
11 <path d="M8.743 10.907 5 7.164V8a3 3 0 0 0 3.743 2.907" />
12 </svg>
13);
14export default SvgMicDisabledIcon;
Added view/app/src/vendor/livekit/react/assets/icons/MicIcon.tsx view/app/src/vendor/livekit/react/assets/icons/MicIcon.tsx +18 −0 View view/app/src/vendor/livekit/react/assets/icons/MicIcon.tsx
@@ -0,0 +1,18 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgMicIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="currentColor" {...props}>
10 <path
11 fillRule="evenodd"
12 d="M2.975 8.002a.5.5 0 0 1 .547.449 4.5 4.5 0 0 0 8.956 0 .5.5 0 1 1 .995.098A5.5 5.5 0 0 1 8.5 13.478V15h2a.5.5 0 0 1 0 1h-5a.5.5 0 0 1 0-1h2v-1.522a5.5 5.5 0 0 1-4.973-4.929.5.5 0 0 1 .448-.547"
13 clipRule="evenodd"
14 />
15 <path d="M5 3a3 3 0 1 1 6 0v5a3 3 0 0 1-6 0z" />
16 </svg>
17);
18export default SvgMicIcon;
Added view/app/src/vendor/livekit/react/assets/icons/QualityExcellentIcon.tsx view/app/src/vendor/livekit/react/assets/icons/QualityExcellentIcon.tsx +14 −0 View view/app/src/vendor/livekit/react/assets/icons/QualityExcellentIcon.tsx
@@ -0,0 +1,14 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgQualityExcellentIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="currentcolor" {...props}>
10 <path d="M0 11.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5zm6-5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5zm6-6a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5z" />
11 <path d="M0 11.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5zm6-5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5zm6-6a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5z" />
12 </svg>
13);
14export default SvgQualityExcellentIcon;
Added view/app/src/vendor/livekit/react/assets/icons/QualityGoodIcon.tsx view/app/src/vendor/livekit/react/assets/icons/QualityGoodIcon.tsx +18 −0 View view/app/src/vendor/livekit/react/assets/icons/QualityGoodIcon.tsx
@@ -0,0 +1,18 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgQualityGoodIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="currentcolor" {...props}>
10 <path d="M0 11.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5zm6-5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5z" />
11 <path d="M0 11.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5zm6-5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5z" />
12 <g opacity={0.25}>
13 <path d="M12 .5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5z" />
14 <path d="M12 .5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5z" />
15 </g>
16 </svg>
17);
18export default SvgQualityGoodIcon;
Added view/app/src/vendor/livekit/react/assets/icons/QualityPoorIcon.tsx view/app/src/vendor/livekit/react/assets/icons/QualityPoorIcon.tsx +19 −0 View view/app/src/vendor/livekit/react/assets/icons/QualityPoorIcon.tsx
@@ -0,0 +1,19 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgQualityPoorIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="currentcolor" {...props}>
10 <path d="M0 11.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5z" />
11 <path d="M0 11.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5z" />
12 <g opacity={0.25}>
13 <path d="M6 6.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5z" />
14 <path d="M6 6.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5zm6-6a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5z" />
15 <path d="M12 .5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5z" />
16 </g>
17 </svg>
18);
19export default SvgQualityPoorIcon;
Added view/app/src/vendor/livekit/react/assets/icons/QualityUnknownIcon.tsx view/app/src/vendor/livekit/react/assets/icons/QualityUnknownIcon.tsx +16 −0 View view/app/src/vendor/livekit/react/assets/icons/QualityUnknownIcon.tsx
@@ -0,0 +1,16 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgQualityUnknownIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="currentColor" {...props}>
10 <g opacity={0.25}>
11 <path d="M0 11.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5zm6-5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5zm6-6a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5z" />
12 <path d="M0 11.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5zm6-5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5zm6-6a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5z" />
13 </g>
14 </svg>
15);
16export default SvgQualityUnknownIcon;
Added view/app/src/vendor/livekit/react/assets/icons/ScreenShareIcon.tsx view/app/src/vendor/livekit/react/assets/icons/ScreenShareIcon.tsx +24 −0 View view/app/src/vendor/livekit/react/assets/icons/ScreenShareIcon.tsx
@@ -0,0 +1,24 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgScreenShareIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={20} height={16} fill="none" {...props}>
10 <path
11 fill="currentColor"
12 fillRule="evenodd"
13 d="M0 2.75A2.75 2.75 0 0 1 2.75 0h14.5A2.75 2.75 0 0 1 20 2.75v10.5A2.75 2.75 0 0 1 17.25 16H2.75A2.75 2.75 0 0 1 0 13.25zM2.75 1.5c-.69 0-1.25.56-1.25 1.25v10.5c0 .69.56 1.25 1.25 1.25h14.5c.69 0 1.25-.56 1.25-1.25V2.75c0-.69-.56-1.25-1.25-1.25z"
14 clipRule="evenodd"
15 />
16 <path
17 fill="currentColor"
18 fillRule="evenodd"
19 d="M9.47 4.22a.75.75 0 0 1 1.06 0l2.25 2.25a.75.75 0 0 1-1.06 1.06l-.97-.97v4.69a.75.75 0 0 1-1.5 0V6.56l-.97.97a.75.75 0 0 1-1.06-1.06z"
20 clipRule="evenodd"
21 />
22 </svg>
23);
24export default SvgScreenShareIcon;
Added view/app/src/vendor/livekit/react/assets/icons/ScreenShareStopIcon.tsx view/app/src/vendor/livekit/react/assets/icons/ScreenShareStopIcon.tsx +20 −0 View view/app/src/vendor/livekit/react/assets/icons/ScreenShareStopIcon.tsx
@@ -0,0 +1,20 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgScreenShareStopIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={20} height={16} fill="none" {...props}>
10 <g fill="currentColor">
11 <path d="M7.28 4.22a.75.75 0 0 0-1.06 1.06L8.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L10 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L11.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L10 6.94z" />
12 <path
13 fillRule="evenodd"
14 d="M2.75 0A2.75 2.75 0 0 0 0 2.75v10.5A2.75 2.75 0 0 0 2.75 16h14.5A2.75 2.75 0 0 0 20 13.25V2.75A2.75 2.75 0 0 0 17.25 0zM1.5 2.75c0-.69.56-1.25 1.25-1.25h14.5c.69 0 1.25.56 1.25 1.25v10.5c0 .69-.56 1.25-1.25 1.25H2.75c-.69 0-1.25-.56-1.25-1.25z"
15 clipRule="evenodd"
16 />
17 </g>
18 </svg>
19);
20export default SvgScreenShareStopIcon;
Added view/app/src/vendor/livekit/react/assets/icons/SpinnerIcon.tsx view/app/src/vendor/livekit/react/assets/icons/SpinnerIcon.tsx +92 −0 View view/app/src/vendor/livekit/react/assets/icons/SpinnerIcon.tsx
@@ -0,0 +1,92 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgSpinnerIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="none" {...props}>
10 <path
11 fill="currentColor"
12 fillRule="evenodd"
13 d="M8 0a.75.75 0 0 1 .75.75v2.5a.75.75 0 0 1-1.5 0V.75A.75.75 0 0 1 8 0"
14 clipRule="evenodd"
15 />
16 <path
17 fill="currentColor"
18 fillRule="evenodd"
19 d="M8 12a.75.75 0 0 1 .75.75v2.5a.75.75 0 0 1-1.5 0v-2.5A.75.75 0 0 1 8 12"
20 clipRule="evenodd"
21 opacity={0.7}
22 />
23 <path
24 fill="currentColor"
25 fillRule="evenodd"
26 d="M12 1.072a.75.75 0 0 1 .274 1.024l-1.25 2.165a.75.75 0 0 1-1.299-.75l1.25-2.165A.75.75 0 0 1 12 1.072"
27 clipRule="evenodd"
28 />
29 <path
30 fill="currentColor"
31 fillRule="evenodd"
32 d="M6 11.464a.75.75 0 0 1 .274 1.025l-1.25 2.165a.75.75 0 0 1-1.299-.75l1.25-2.165A.75.75 0 0 1 6 11.464"
33 clipRule="evenodd"
34 opacity={0.6}
35 />
36 <path
37 fill="currentColor"
38 fillRule="evenodd"
39 d="M14.928 4a.75.75 0 0 1-.274 1.025l-2.165 1.25a.75.75 0 1 1-.75-1.3l2.165-1.25A.75.75 0 0 1 14.928 4"
40 clipRule="evenodd"
41 />
42 <path
43 fill="currentColor"
44 fillRule="evenodd"
45 d="M4.536 10a.75.75 0 0 1-.275 1.024l-2.165 1.25a.75.75 0 0 1-.75-1.298l2.165-1.25A.75.75 0 0 1 4.536 10"
46 clipRule="evenodd"
47 opacity={0.5}
48 />
49 <path
50 fill="currentColor"
51 fillRule="evenodd"
52 d="M16 8a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h2.5A.75.75 0 0 1 16 8"
53 clipRule="evenodd"
54 />
55 <path
56 fill="currentColor"
57 fillRule="evenodd"
58 d="M4 8a.75.75 0 0 1-.75.75H.75a.75.75 0 0 1 0-1.5h2.5A.75.75 0 0 1 4 8"
59 clipRule="evenodd"
60 opacity={0.4}
61 />
62 <path
63 fill="currentColor"
64 fillRule="evenodd"
65 d="M14.928 12a.75.75 0 0 1-1.024.274l-2.165-1.25a.75.75 0 0 1 .75-1.299l2.165 1.25A.75.75 0 0 1 14.928 12"
66 clipRule="evenodd"
67 opacity={0.9}
68 />
69 <path
70 fill="currentColor"
71 fillRule="evenodd"
72 d="M4.536 6a.75.75 0 0 1-1.025.275l-2.165-1.25a.75.75 0 1 1 .75-1.3l2.165 1.25A.75.75 0 0 1 4.536 6"
73 clipRule="evenodd"
74 opacity={0.3}
75 />
76 <path
77 fill="currentColor"
78 fillRule="evenodd"
79 d="M12 14.928a.75.75 0 0 1-1.024-.274l-1.25-2.165a.75.75 0 0 1 1.298-.75l1.25 2.165A.75.75 0 0 1 12 14.928"
80 clipRule="evenodd"
81 opacity={0.8}
82 />
83 <path
84 fill="currentColor"
85 fillRule="evenodd"
86 d="M6 4.536a.75.75 0 0 1-1.024-.275l-1.25-2.165a.75.75 0 1 1 1.299-.75l1.25 2.165A.75.75 0 0 1 6 4.536"
87 clipRule="evenodd"
88 opacity={0.2}
89 />
90 </svg>
91);
92export default SvgSpinnerIcon;
Added view/app/src/vendor/livekit/react/assets/icons/UnfocusToggleIcon.tsx view/app/src/vendor/livekit/react/assets/icons/UnfocusToggleIcon.tsx +15 −0 View view/app/src/vendor/livekit/react/assets/icons/UnfocusToggleIcon.tsx
@@ -0,0 +1,15 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgUnfocusToggleIcon = (props: SVGProps<SVGSVGElement>) => (
9 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="none" {...props}>
10 <g stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}>
11 <path d="M13.25 7H9m0 0V2.75M9 7l5.25-5.25M2.75 9H7m0 0v4.25M7 9l-5.25 5.25" />
12 </g>
13 </svg>
14);
15export default SvgUnfocusToggleIcon;
Added view/app/src/vendor/livekit/react/assets/icons/util.tsx view/app/src/vendor/livekit/react/assets/icons/util.tsx +46 −0 View view/app/src/vendor/livekit/react/assets/icons/util.tsx
@@ -0,0 +1,46 @@
1import { ConnectionQuality, Track } from 'livekit-client';
2
3import {
4 MicIcon,
5 MicDisabledIcon,
6 CameraIcon,
7 CameraDisabledIcon,
8 QualityUnknownIcon,
9 QualityExcellentIcon,
10 QualityGoodIcon,
11 QualityPoorIcon,
12 ScreenShareIcon,
13 ScreenShareStopIcon,
14} from './index';
15
16/**
17 * @internal
18 */
19export function getSourceIcon(source: Track.Source, enabled: boolean) {
20 switch (source) {
21 case Track.Source.Microphone:
22 return enabled ? <MicIcon /> : <MicDisabledIcon />;
23 case Track.Source.Camera:
24 return enabled ? <CameraIcon /> : <CameraDisabledIcon />;
25 case Track.Source.ScreenShare:
26 return enabled ? <ScreenShareStopIcon /> : <ScreenShareIcon />;
27 default:
28 return undefined;
29 }
30}
31
32/**
33 * @internal
34 */
35export function getConnectionQualityIcon(quality: ConnectionQuality) {
36 switch (quality) {
37 case ConnectionQuality.Excellent:
38 return <QualityExcellentIcon />;
39 case ConnectionQuality.Good:
40 return <QualityGoodIcon />;
41 case ConnectionQuality.Poor:
42 return <QualityPoorIcon />;
43 default:
44 return <QualityUnknownIcon />;
45 }
46}
Added view/app/src/vendor/livekit/react/assets/images/index.ts view/app/src/vendor/livekit/react/assets/images/index.ts +1 −0 View view/app/src/vendor/livekit/react/assets/images/index.ts
@@ -0,0 +1 @@
1export { default as ParticipantPlaceholder } from './ParticipantPlaceholder';
Added view/app/src/vendor/livekit/react/assets/images/ParticipantPlaceholder.tsx view/app/src/vendor/livekit/react/assets/images/ParticipantPlaceholder.tsx +30 −0 View view/app/src/vendor/livekit/react/assets/images/ParticipantPlaceholder.tsx
@@ -0,0 +1,30 @@
1/**
2 * WARNING: This file was auto-generated by svgr. Do not edit.
3 */
4import type { SVGProps } from 'react';
5/**
6 * @internal
7 */
8const SvgParticipantPlaceholder = (props: SVGProps<SVGSVGElement>) => (
9 <svg
10 width={320}
11 height={320}
12 viewBox="0 0 320 320"
13 preserveAspectRatio="xMidYMid meet"
14 fill="none"
15 xmlns="http://www.w3.org/2000/svg"
16 {...props}
17 >
18 <path
19 d="M160 180C204.182 180 240 144.183 240 100C240 55.8172 204.182 20 160 20C115.817 20 79.9997 55.8172 79.9997 100C79.9997 144.183 115.817 180 160 180Z"
20 fill="white"
21 fillOpacity={0.25}
22 />
23 <path
24 d="M97.6542 194.614C103.267 191.818 109.841 192.481 115.519 195.141C129.025 201.466 144.1 205 159.999 205C175.899 205 190.973 201.466 204.48 195.141C210.158 192.481 216.732 191.818 222.345 194.614C262.703 214.719 291.985 253.736 298.591 300.062C300.15 310.997 291.045 320 280 320H39.9997C28.954 320 19.8495 310.997 21.4087 300.062C28.014 253.736 57.2966 214.72 97.6542 194.614Z"
25 fill="white"
26 fillOpacity={0.25}
27 />
28 </svg>
29);
30export default SvgParticipantPlaceholder;
Added view/app/src/vendor/livekit/react/components/controls/ChatToggle.tsx view/app/src/vendor/livekit/react/components/controls/ChatToggle.tsx +32 −0 View view/app/src/vendor/livekit/react/components/controls/ChatToggle.tsx
@@ -0,0 +1,32 @@
1import * as React from 'react';
2import { useChatToggle } from '../../hooks';
3
4/** @public */
5export interface ChatToggleProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
6
7/**
8 * The `ChatToggle` component is a button that toggles the visibility of the `Chat` component.
9 * @remarks
10 * For the component to have any effect it has to live inside a `LayoutContext` context.
11 *
12 * @example
13 * ```tsx
14 * <LiveKitRoom>
15 * <ChatToggle />
16 * </LiveKitRoom>
17 * ```
18 * @public
19 */
20export const ChatToggle: (
21 props: ChatToggleProps & React.RefAttributes<HTMLButtonElement>,
22) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLButtonElement, ChatToggleProps>(
23 function ChatToggle(props: ChatToggleProps, ref) {
24 const { mergedProps } = useChatToggle({ props });
25
26 return (
27 <button ref={ref} {...mergedProps}>
28 {props.children}
29 </button>
30 );
31 },
32);
Added view/app/src/vendor/livekit/react/components/controls/ClearPinButton.tsx view/app/src/vendor/livekit/react/components/controls/ClearPinButton.tsx +32 −0 View view/app/src/vendor/livekit/react/components/controls/ClearPinButton.tsx
@@ -0,0 +1,32 @@
1import * as React from 'react';
2import { useClearPinButton } from '../../hooks';
3
4/** @public */
5export interface ClearPinButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
6
7/**
8 * The `ClearPinButton` is a basic html button with the added ability to signal
9 * the `LayoutContext` that it should display the grid view again.
10 * @remarks
11 * This component works only inside a `LayoutContext`.
12 *
13 * @example
14 * ```tsx
15 * <LiveKitRoom>
16 * <ClearPinButton>Back to grid view</ClearPinButton>
17 * </LiveKitRoom>
18 * ```
19 * @public
20 */
21export const ClearPinButton: (
22 props: ClearPinButtonProps & React.RefAttributes<HTMLButtonElement>,
23) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLButtonElement, ClearPinButtonProps>(
24 function ClearPinButton(props: ClearPinButtonProps, ref) {
25 const { buttonProps } = useClearPinButton(props);
26 return (
27 <button ref={ref} {...buttonProps}>
28 {props.children}
29 </button>
30 );
31 },
32);
Added view/app/src/vendor/livekit/react/components/controls/DisconnectButton.tsx view/app/src/vendor/livekit/react/components/controls/DisconnectButton.tsx +32 −0 View view/app/src/vendor/livekit/react/components/controls/DisconnectButton.tsx
@@ -0,0 +1,32 @@
1import * as React from 'react';
2import { useDisconnectButton } from '../../hooks';
3
4/** @public */
5export interface DisconnectButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
6 stopTracks?: boolean;
7}
8
9/**
10 * The `DisconnectButton` is a basic html button with the added ability to disconnect from a LiveKit room.
11 * Normally this is the big red button that allows end users to leave the video or audio call.
12 *
13 * @example
14 * ```tsx
15 * <LiveKitRoom>
16 * <DisconnectButton>Leave room</DisconnectButton>
17 * </LiveKitRoom>
18 * ```
19 * @public
20 */
21export const DisconnectButton: (
22 props: DisconnectButtonProps & React.RefAttributes<HTMLButtonElement>,
23) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLButtonElement, DisconnectButtonProps>(
24 function DisconnectButton(props: DisconnectButtonProps, ref) {
25 const { buttonProps } = useDisconnectButton(props);
26 return (
27 <button ref={ref} {...buttonProps}>
28 {props.children}
29 </button>
30 );
31 },
32);
Added view/app/src/vendor/livekit/react/components/controls/FocusToggle.tsx view/app/src/vendor/livekit/react/components/controls/FocusToggle.tsx +54 −0 View view/app/src/vendor/livekit/react/components/controls/FocusToggle.tsx
@@ -0,0 +1,54 @@
1import * as React from 'react';
2import { LayoutContext, useMaybeTrackRefContext } from '../../context';
3import { FocusToggleIcon, UnfocusToggleIcon } from '../../assets/icons';
4import { useFocusToggle } from '../../hooks';
5import type { TrackReferenceOrPlaceholder } from '@livekit/components-core';
6
7/** @public */
8export interface FocusToggleProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
9 trackRef?: TrackReferenceOrPlaceholder;
10}
11
12/**
13 * The `FocusToggle` puts the `ParticipantTile` in focus or removes it from focus.
14 * @remarks
15 * This component needs to live inside `LayoutContext` to work properly.
16 *
17 * @example
18 * ```tsx
19 * <ParticipantTile>
20 * <FocusToggle />
21 * </ParticipantTile>
22 * ```
23 * @public
24 */
25export const FocusToggle: (
26 props: FocusToggleProps & React.RefAttributes<HTMLButtonElement>,
27) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLButtonElement, FocusToggleProps>(
28 function FocusToggle({ trackRef, ...props }: FocusToggleProps, ref) {
29 const trackRefFromContext = useMaybeTrackRefContext();
30
31 const { mergedProps, inFocus } = useFocusToggle({
32 trackRef: trackRef ?? trackRefFromContext,
33 props,
34 });
35
36 return (
37 <LayoutContext.Consumer>
38 {(layoutContext) =>
39 layoutContext !== undefined && (
40 <button ref={ref} {...mergedProps}>
41 {props.children ? (
42 props.children
43 ) : inFocus ? (
44 <UnfocusToggleIcon />
45 ) : (
46 <FocusToggleIcon />
47 )}
48 </button>
49 )
50 }
51 </LayoutContext.Consumer>
52 );
53 },
54);
Added view/app/src/vendor/livekit/react/components/controls/MediaDeviceSelect.tsx view/app/src/vendor/livekit/react/components/controls/MediaDeviceSelect.tsx +146 −0 View view/app/src/vendor/livekit/react/components/controls/MediaDeviceSelect.tsx
@@ -0,0 +1,146 @@
1import * as React from 'react';
2import { useMaybeRoomContext } from '../../context';
3import { mergeProps } from '../../utils';
4import { RoomEvent, type LocalAudioTrack, type LocalVideoTrack } from 'livekit-client';
5import { useMediaDeviceSelect } from '../../hooks';
6
7/** @public */
8export interface MediaDeviceSelectProps extends Omit<
9 React.HTMLAttributes<HTMLUListElement>,
10 'onError'
11> {
12 kind: MediaDeviceKind;
13 onActiveDeviceChange?: (deviceId: string) => void;
14 onDeviceListChange?: (devices: MediaDeviceInfo[]) => void;
15 onDeviceSelectError?: (e: Error) => void;
16 initialSelection?: string;
17 /** will force the browser to only return the specified device
18 * will call `onDeviceSelectError` with the error in case this fails
19 */
20 exactMatch?: boolean;
21 track?: LocalAudioTrack | LocalVideoTrack;
22 /**
23 * this will call getUserMedia if the permissions are not yet given to enumerate the devices with device labels.
24 * in some browsers multiple calls to getUserMedia result in multiple permission prompts.
25 * It's generally advised only flip this to true, once a (preview) track has been acquired successfully with the
26 * appropriate permissions.
27 *
28 * @see {@link MediaDeviceMenu}
29 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices | MDN enumerateDevices}
30 */
31 requestPermissions?: boolean;
32 onError?: (e: Error) => void;
33}
34
35/**
36 * The `MediaDeviceSelect` list all media devices of one kind.
37 * Clicking on one of the listed devices make it the active media device.
38 *
39 * @example
40 * ```tsx
41 * <LiveKitRoom>
42 * <MediaDeviceSelect kind='audioinput' />
43 * </LiveKitRoom>
44 * ```
45 * @public
46 */
47export const MediaDeviceSelect: (
48 props: MediaDeviceSelectProps & React.RefAttributes<HTMLUListElement>,
49) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLUListElement, MediaDeviceSelectProps>(
50 function MediaDeviceSelect(
51 {
52 kind,
53 initialSelection,
54 onActiveDeviceChange,
55 onDeviceListChange,
56 onDeviceSelectError,
57 exactMatch,
58 track,
59 requestPermissions,
60 onError,
61 ...props
62 }: MediaDeviceSelectProps,
63 ref,
64 ) {
65 const room = useMaybeRoomContext();
66 const previousActiveDeviceId = React.useRef<string>('default');
67 const handleError = React.useCallback(
68 (e: Error) => {
69 if (room) {
70 // awkwardly emit the event from outside of the room, as we don't have other means to raise a MediaDeviceError
71 room.emit(RoomEvent.MediaDevicesError, e);
72 }
73 onError?.(e);
74 },
75 [room, onError],
76 );
77 const { devices, activeDeviceId, setActiveMediaDevice, className } = useMediaDeviceSelect({
78 kind,
79 room,
80 track,
81 requestPermissions,
82 onError: handleError,
83 });
84 React.useEffect(() => {
85 if (initialSelection !== undefined) {
86 setActiveMediaDevice(initialSelection);
87 }
88 }, [setActiveMediaDevice]);
89
90 React.useEffect(() => {
91 if (typeof onDeviceListChange === 'function') {
92 onDeviceListChange(devices);
93 }
94 }, [onDeviceListChange, devices]);
95
96 React.useEffect(() => {
97 if (activeDeviceId !== previousActiveDeviceId.current) {
98 onActiveDeviceChange?.(activeDeviceId);
99 }
100 previousActiveDeviceId.current = activeDeviceId;
101 }, [activeDeviceId]);
102
103 const handleActiveDeviceChange = async (deviceId: string) => {
104 try {
105 await setActiveMediaDevice(deviceId, { exact: exactMatch ?? true });
106 } catch (e) {
107 if (e instanceof Error) {
108 onDeviceSelectError?.(e);
109 } else {
110 throw e;
111 }
112 }
113 };
114 // Merge Props
115 const mergedProps = React.useMemo(
116 () => mergeProps(props, { className }, { className: 'lk-list' }),
117 [className, props],
118 );
119
120 const hasDefault = !!devices.find((info) => info.label.toLowerCase().startsWith('default'));
121
122 function isActive(deviceId: string, activeDeviceId: string, index: number) {
123 return (
124 deviceId === activeDeviceId || (!hasDefault && index === 0 && activeDeviceId === 'default')
125 );
126 }
127
128 return (
129 <ul ref={ref} {...mergedProps}>
130 {devices.map((device, index) => (
131 <li
132 key={device.deviceId}
133 id={device.deviceId}
134 data-lk-active={isActive(device.deviceId, activeDeviceId, index)}
135 aria-selected={isActive(device.deviceId, activeDeviceId, index)}
136 role="option"
137 >
138 <button className="lk-button" onClick={() => handleActiveDeviceChange(device.deviceId)}>
139 {device.label}
140 </button>
141 </li>
142 ))}
143 </ul>
144 );
145 },
146);
Added view/app/src/vendor/livekit/react/components/controls/PaginationControl.tsx view/app/src/vendor/livekit/react/components/controls/PaginationControl.tsx +49 −0 View view/app/src/vendor/livekit/react/components/controls/PaginationControl.tsx
@@ -0,0 +1,49 @@
1import * as React from 'react';
2import SvgChevron from '../../assets/icons/Chevron';
3import type { usePagination } from '../../hooks';
4import { createInteractingObservable } from '@livekit/components-core';
5
6export interface PaginationControlProps extends Pick<
7 ReturnType<typeof usePagination>,
8 'totalPageCount' | 'nextPage' | 'prevPage' | 'currentPage'
9> {
10 /** Reference to an HTML element that holds the pages, while interacting (`mouseover`)
11 * with it, the pagination controls will appear for a while. */
12 pagesContainer?: React.RefObject<HTMLElement | null>;
13}
14
15export function PaginationControl({
16 totalPageCount,
17 nextPage,
18 prevPage,
19 currentPage,
20 pagesContainer: connectedElement,
21}: PaginationControlProps) {
22 const [interactive, setInteractive] = React.useState(false);
23 React.useEffect(() => {
24 let subscription:
25 ReturnType<ReturnType<typeof createInteractingObservable>['subscribe']> | undefined;
26 if (connectedElement) {
27 subscription = createInteractingObservable(connectedElement.current, 2000).subscribe(
28 setInteractive,
29 );
30 }
31 return () => {
32 if (subscription) {
33 subscription.unsubscribe();
34 }
35 };
36 }, [connectedElement]);
37
38 return (
39 <div className="lk-pagination-control" data-lk-user-interaction={interactive}>
40 <button className="lk-button" onClick={prevPage}>
41 <SvgChevron />
42 </button>
43 <span className="lk-pagination-count">{`${currentPage} of ${totalPageCount}`}</span>
44 <button className="lk-button" onClick={nextPage}>
45 <SvgChevron />
46 </button>
47 </div>
48 );
49}
Added view/app/src/vendor/livekit/react/components/controls/PaginationIndicator.tsx view/app/src/vendor/livekit/react/components/controls/PaginationIndicator.tsx +26 −0 View view/app/src/vendor/livekit/react/components/controls/PaginationIndicator.tsx
@@ -0,0 +1,26 @@
1import * as React from 'react';
2
3export interface PaginationIndicatorProps {
4 totalPageCount: number;
5 currentPage: number;
6}
7
8export const PaginationIndicator: (
9 props: PaginationIndicatorProps & React.RefAttributes<HTMLDivElement>,
10) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLDivElement, PaginationIndicatorProps>(
11 function PaginationIndicator({ totalPageCount, currentPage }: PaginationIndicatorProps, ref) {
12 const bubbles = new Array(totalPageCount).fill('').map((_, index) => {
13 if (index + 1 === currentPage) {
14 return <span data-lk-active key={index} />;
15 } else {
16 return <span key={index} />;
17 }
18 });
19
20 return (
21 <div ref={ref} className="lk-pagination-indicator">
22 {bubbles}
23 </div>
24 );
25 },
26);
Added view/app/src/vendor/livekit/react/components/controls/SettingsMenuToggle.tsx view/app/src/vendor/livekit/react/components/controls/SettingsMenuToggle.tsx +26 −0 View view/app/src/vendor/livekit/react/components/controls/SettingsMenuToggle.tsx
@@ -0,0 +1,26 @@
1import * as React from 'react';
2import { useSettingsToggle } from '../../hooks/useSettingsToggle';
3
4/** @alpha */
5export interface SettingsMenuToggleProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
6
7/**
8 * The `SettingsMenuToggle` component is a button that toggles the visibility of the `SettingsMenu` component.
9 * @remarks
10 * For the component to have any effect it has to live inside a `LayoutContext` context.
11 *
12 * @alpha
13 */
14export const SettingsMenuToggle: (
15 props: SettingsMenuToggleProps & React.RefAttributes<HTMLButtonElement>,
16) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLButtonElement, SettingsMenuToggleProps>(
17 function SettingsMenuToggle(props: SettingsMenuToggleProps, ref) {
18 const { mergedProps } = useSettingsToggle({ props });
19
20 return (
21 <button ref={ref} {...mergedProps}>
22 {props.children}
23 </button>
24 );
25 },
26);
Added view/app/src/vendor/livekit/react/components/controls/StartAudio.tsx view/app/src/vendor/livekit/react/components/controls/StartAudio.tsx +41 −0 View view/app/src/vendor/livekit/react/components/controls/StartAudio.tsx
@@ -0,0 +1,41 @@
1import * as React from 'react';
2import { useEnsureRoom } from '../../context';
3import { useStartAudio } from '../../hooks';
4import { Room } from 'livekit-client';
5import { lkT } from '@app/vendor/livekit/lk-i18n';
6
7/** @public */
8export interface AllowAudioPlaybackProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
9 room?: Room;
10 label: string;
11}
12
13/**
14 * The `StartAudio` component is only visible when the browser blocks audio playback. This is due to some browser implemented autoplay policies.
15 * To start audio playback, the user must perform a user-initiated event such as clicking this button.
16 * As soon as audio playback starts, the button hides itself again.
17 *
18 * @example
19 * ```tsx
20 * <LiveKitRoom>
21 * <StartAudio label="Click to allow audio playback" />
22 * </LiveKitRoom>
23 * ```
24 *
25 * @see Autoplay policy on MDN web docs: {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy}
26 * @public
27 */
28export const StartAudio: (
29 props: AllowAudioPlaybackProps & React.RefAttributes<HTMLButtonElement>,
30) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLButtonElement, AllowAudioPlaybackProps>(
31 function StartAudio({ label = lkT("livekit.allowAudio"), ...props }: AllowAudioPlaybackProps, ref) {
32 const room = useEnsureRoom(props.room);
33 const { mergedProps } = useStartAudio({ room, props });
34
35 return (
36 <button ref={ref} {...mergedProps}>
37 {label}
38 </button>
39 );
40 },
41);
Added view/app/src/vendor/livekit/react/components/controls/StartMediaButton.tsx view/app/src/vendor/livekit/react/components/controls/StartMediaButton.tsx +42 −0 View view/app/src/vendor/livekit/react/components/controls/StartMediaButton.tsx
@@ -0,0 +1,42 @@
1import * as React from 'react';
2import { useRoomContext } from '../../context';
3import { useStartAudio, useStartVideo } from '../../hooks';
4import { lkT } from '@app/vendor/livekit/lk-i18n';
5
6/** @public */
7export interface AllowMediaPlaybackProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
8 label?: string;
9}
10
11/**
12 * The `StartMediaButton` component is only visible when the browser blocks media playback. This is due to some browser implemented autoplay policies.
13 * To start media playback, the user must perform a user-initiated event such as clicking this button.
14 * As soon as media playback starts, the button hides itself again.
15 *
16 * @example
17 * ```tsx
18 * <LiveKitRoom>
19 * <StartMediaButton label="Click to allow media playback" />
20 * </LiveKitRoom>
21 * ```
22 *
23 * @see Autoplay policy on MDN web docs: {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy}
24 * @public
25 */
26export const StartMediaButton: (
27 props: AllowMediaPlaybackProps & React.RefAttributes<HTMLButtonElement>,
28) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLButtonElement, AllowMediaPlaybackProps>(
29 function StartMediaButton({ label, ...props }: AllowMediaPlaybackProps, ref) {
30 const room = useRoomContext();
31 const { mergedProps: audioProps, canPlayAudio } = useStartAudio({ room, props });
32 const { mergedProps, canPlayVideo } = useStartVideo({ room, props: audioProps });
33 const { style, ...restProps } = mergedProps;
34 style.display = canPlayAudio && canPlayVideo ? 'none' : 'block';
35
36 return (
37 <button ref={ref} style={style} {...restProps}>
38 {label ?? lkT(canPlayAudio ? "livekit.startVideo" : "livekit.startAudio")}
39 </button>
40 );
41 },
42);
Added view/app/src/vendor/livekit/react/components/controls/TrackToggle.tsx view/app/src/vendor/livekit/react/components/controls/TrackToggle.tsx +56 −0 View view/app/src/vendor/livekit/react/components/controls/TrackToggle.tsx
@@ -0,0 +1,56 @@
1import type { CaptureOptionsBySource, ToggleSource } from '@livekit/components-core';
2import * as React from 'react';
3import { getSourceIcon } from '../../assets/icons/util';
4import { useTrackToggle } from '../../hooks';
5import { type TrackPublishOptions } from 'livekit-client';
6
7/** @public */
8export interface TrackToggleProps<T extends ToggleSource> extends Omit<
9 React.ButtonHTMLAttributes<HTMLButtonElement>,
10 'onChange'
11> {
12 source: T;
13 showIcon?: boolean;
14 initialState?: boolean;
15 /**
16 * Function that is called when the enabled state of the toggle changes.
17 * The second function argument `isUserInitiated` is `true` if the change was initiated by a user interaction, such as a click.
18 */
19 onChange?: (enabled: boolean, isUserInitiated: boolean) => void;
20 captureOptions?: CaptureOptionsBySource<T>;
21 publishOptions?: TrackPublishOptions;
22 onDeviceError?: (error: Error) => void;
23}
24
25/**
26 * With the `TrackToggle` component it is possible to mute and unmute your camera and microphone.
27 * The component uses an html button element under the hood so you can treat it like a button.
28 *
29 * @example
30 * ```tsx
31 * <LiveKitRoom>
32 * <TrackToggle source={Track.Source.Microphone} />
33 * <TrackToggle source={Track.Source.Camera} />
34 * </LiveKitRoom>
35 * ```
36 * @public
37 */
38export const TrackToggle: <T extends ToggleSource>(
39 props: TrackToggleProps<T> & React.RefAttributes<HTMLButtonElement>,
40) => React.ReactNode = /* @__PURE__ */ React.forwardRef(function TrackToggle<
41 T extends ToggleSource,
42>({ showIcon, ...props }: TrackToggleProps<T>, ref: React.ForwardedRef<HTMLButtonElement>) {
43 const { buttonProps, enabled } = useTrackToggle(props);
44 const [isClient, setIsClient] = React.useState(false);
45 React.useEffect(() => {
46 setIsClient(true);
47 }, []);
48 return (
49 isClient && (
50 <button ref={ref} {...buttonProps}>
51 {(showIcon ?? true) && getSourceIcon(props.source, enabled)}
52 {props.children}
53 </button>
54 )
55 );
56});
Added view/app/src/vendor/livekit/react/components/layout/CarouselLayout.tsx view/app/src/vendor/livekit/react/components/layout/CarouselLayout.tsx +80 −0 View view/app/src/vendor/livekit/react/components/layout/CarouselLayout.tsx
@@ -0,0 +1,80 @@
1import type { TrackReferenceOrPlaceholder } from '@livekit/components-core';
2import { getScrollBarWidth } from '@livekit/components-core';
3import * as React from 'react';
4import { useSize } from '../../hooks/internal';
5import { useVisualStableUpdate } from '../../hooks';
6import { TrackLoop } from '../TrackLoop';
7
8const MIN_HEIGHT = 130;
9const MIN_WIDTH = 140;
10const MIN_VISIBLE_TILES = 1;
11const ASPECT_RATIO = 16 / 10;
12const ASPECT_RATIO_INVERT = (1 - ASPECT_RATIO) * -1;
13
14/** @public */
15export interface CarouselLayoutProps extends React.HTMLAttributes<HTMLMediaElement> {
16 tracks: TrackReferenceOrPlaceholder[];
17 children: React.ReactNode;
18 /** Place the tiles vertically or horizontally next to each other.
19 * If undefined orientation is guessed by the dimensions of the container. */
20 orientation?: 'vertical' | 'horizontal';
21}
22
23/**
24 * The `CarouselLayout` component displays a list of tracks in a scroll container.
25 * It will display as many tiles as possible and overflow the rest.
26 * @remarks
27 * To ensure visual stability when tiles are reordered due to track updates,
28 * the component uses the `useVisualStableUpdate` hook.
29 * @example
30 * ```tsx
31 * const tracks = useTracks([Track.Source.Camera]);
32 * <CarouselLayout tracks={tracks}>
33 * <ParticipantTile />
34 * </CarouselLayout>
35 * ```
36 * @public
37 */
38export function CarouselLayout({ tracks, orientation, ...props }: CarouselLayoutProps) {
39 const asideEl = React.useRef<HTMLDivElement>(null);
40 const [prevTiles, setPrevTiles] = React.useState(0);
41 const { width, height } = useSize(asideEl);
42 const carouselOrientation = orientation
43 ? orientation
44 : height >= width
45 ? 'vertical'
46 : 'horizontal';
47
48 const tileSpan =
49 carouselOrientation === 'vertical'
50 ? Math.max(width * ASPECT_RATIO_INVERT, MIN_HEIGHT)
51 : Math.max(height * ASPECT_RATIO, MIN_WIDTH);
52 const scrollBarWidth = getScrollBarWidth();
53
54 const tilesThatFit =
55 carouselOrientation === 'vertical'
56 ? Math.max((height - scrollBarWidth) / tileSpan, MIN_VISIBLE_TILES)
57 : Math.max((width - scrollBarWidth) / tileSpan, MIN_VISIBLE_TILES);
58
59 let maxVisibleTiles = Math.round(tilesThatFit);
60 if (Math.abs(tilesThatFit - prevTiles) < 0.5) {
61 maxVisibleTiles = Math.round(prevTiles);
62 } else if (prevTiles !== tilesThatFit) {
63 setPrevTiles(tilesThatFit);
64 }
65
66 const sortedTiles = useVisualStableUpdate(tracks, maxVisibleTiles);
67
68 React.useLayoutEffect(() => {
69 if (asideEl.current) {
70 asideEl.current.dataset.lkOrientation = carouselOrientation;
71 asideEl.current.style.setProperty('--lk-max-visible-tiles', maxVisibleTiles.toString());
72 }
73 }, [maxVisibleTiles, carouselOrientation]);
74
75 return (
76 <aside key={carouselOrientation} className="lk-carousel" ref={asideEl} {...props}>
77 <TrackLoop tracks={sortedTiles}>{props.children}</TrackLoop>
78 </aside>
79 );
80}
Added view/app/src/vendor/livekit/react/components/layout/FocusLayout.tsx view/app/src/vendor/livekit/react/components/layout/FocusLayout.tsx +37 −0 View view/app/src/vendor/livekit/react/components/layout/FocusLayout.tsx
@@ -0,0 +1,37 @@
1import * as React from 'react';
2import { mergeProps } from '../../utils';
3import type { TrackReferenceOrPlaceholder } from '@livekit/components-core';
4import { ParticipantTile } from '../participant/ParticipantTile';
5import type { ParticipantClickEvent } from '@livekit/components-core';
6
7/** @public */
8export interface FocusLayoutContainerProps extends React.HTMLAttributes<HTMLDivElement> {}
9
10/**
11 * The `FocusLayoutContainer` is a layout component that expects two children:
12 * A small side component: In a video conference, this is usually a carousel of participants
13 * who are not in focus. And a larger main component to display the focused participant.
14 * For example, with the `FocusLayout` component.
15 * @public
16 */
17export function FocusLayoutContainer(props: FocusLayoutContainerProps) {
18 const elementProps = mergeProps(props, { className: 'lk-focus-layout' });
19
20 return <div {...elementProps}>{props.children}</div>;
21}
22
23/** @public */
24export interface FocusLayoutProps extends React.HTMLAttributes<HTMLElement> {
25 /** The track to display in the focus layout. */
26 trackRef?: TrackReferenceOrPlaceholder;
27
28 onParticipantClick?: (evt: ParticipantClickEvent) => void;
29}
30
31/**
32 * The `FocusLayout` component is just a light wrapper around the `ParticipantTile` to display a single participant.
33 * @public
34 */
35export function FocusLayout({ trackRef, ...htmlProps }: FocusLayoutProps) {
36 return <ParticipantTile trackRef={trackRef} {...htmlProps} />;
37}
Added view/app/src/vendor/livekit/react/components/layout/GridLayout.tsx view/app/src/vendor/livekit/react/components/layout/GridLayout.tsx +62 −0 View view/app/src/vendor/livekit/react/components/layout/GridLayout.tsx
@@ -0,0 +1,62 @@
1import * as React from 'react';
2import type { UseParticipantsOptions } from '../../hooks';
3import { useGridLayout, usePagination, useSwipe } from '../../hooks';
4import { mergeProps } from '../../utils';
5import type { TrackReferenceOrPlaceholder } from '@livekit/components-core';
6import { TrackLoop } from '../TrackLoop';
7import { PaginationControl } from '../controls/PaginationControl';
8import { PaginationIndicator } from '../controls/PaginationIndicator';
9
10/** @public */
11export interface GridLayoutProps
12 extends React.HTMLAttributes<HTMLDivElement>, Pick<UseParticipantsOptions, 'updateOnlyOn'> {
13 children: React.ReactNode;
14 tracks: TrackReferenceOrPlaceholder[];
15}
16
17/**
18 * The `GridLayout` component displays the nested participants in a grid where every participants has the same size.
19 * It also supports pagination if there are more participants than the grid can display.
20 * @remarks
21 * To ensure visual stability when tiles are reordered due to track updates,
22 * the component uses the `useVisualStableUpdate` hook.
23 * @example
24 * ```tsx
25 * <LiveKitRoom>
26 * <GridLayout tracks={tracks}>
27 * <ParticipantTile />
28 * </GridLayout>
29 * <LiveKitRoom>
30 * ```
31 * @public
32 */
33export function GridLayout({ tracks, ...props }: GridLayoutProps) {
34 const gridEl = React.createRef<HTMLDivElement>();
35
36 const elementProps = React.useMemo(
37 () => mergeProps(props, { className: 'lk-grid-layout' }),
38 [props],
39 );
40 const { layout } = useGridLayout(gridEl, tracks.length);
41 const pagination = usePagination(layout.maxTiles, tracks);
42
43 useSwipe(gridEl, {
44 onLeftSwipe: pagination.nextPage,
45 onRightSwipe: pagination.prevPage,
46 });
47
48 return (
49 <div ref={gridEl} data-lk-pagination={pagination.totalPageCount > 1} {...elementProps}>
50 <TrackLoop tracks={pagination.tracks}>{props.children}</TrackLoop>
51 {tracks.length > layout.maxTiles && (
52 <>
53 <PaginationIndicator
54 totalPageCount={pagination.totalPageCount}
55 currentPage={pagination.currentPage}
56 />
57 <PaginationControl pagesContainer={gridEl} {...pagination} />
58 </>
59 )}
60 </div>
61 );
62}
Added view/app/src/vendor/livekit/react/components/layout/index.ts view/app/src/vendor/livekit/react/components/layout/index.ts +3 −0 View view/app/src/vendor/livekit/react/components/layout/index.ts
@@ -0,0 +1,3 @@
1export * from './FocusLayout';
2export * from './GridLayout';
3export { CarouselLayout, type CarouselLayoutProps } from './CarouselLayout';
Added view/app/src/vendor/livekit/react/components/layout/LayoutContextProvider.tsx view/app/src/vendor/livekit/react/components/layout/LayoutContextProvider.tsx +36 −0 View view/app/src/vendor/livekit/react/components/layout/LayoutContextProvider.tsx
@@ -0,0 +1,36 @@
1import type { PinState, WidgetState } from '@livekit/components-core';
2import { log } from '@livekit/components-core';
3import * as React from 'react';
4import type { LayoutContextType } from '../../context';
5import { LayoutContext, useEnsureCreateLayoutContext } from '../../context';
6
7/** @alpha */
8export interface LayoutContextProviderProps {
9 value?: LayoutContextType;
10 onPinChange?: (state: PinState) => void;
11 onWidgetChange?: (state: WidgetState) => void;
12}
13
14/** @alpha */
15export function LayoutContextProvider({
16 value,
17 onPinChange,
18 onWidgetChange,
19 children,
20}: React.PropsWithChildren<LayoutContextProviderProps>) {
21 const layoutContextValue = useEnsureCreateLayoutContext(value);
22
23 React.useEffect(() => {
24 log.debug('PinState Updated', { state: layoutContextValue.pin.state });
25 if (onPinChange && layoutContextValue.pin.state) onPinChange(layoutContextValue.pin.state);
26 }, [layoutContextValue.pin.state, onPinChange]);
27
28 React.useEffect(() => {
29 log.debug('Widget Updated', { widgetState: layoutContextValue.widget.state });
30 if (onWidgetChange && layoutContextValue.widget.state) {
31 onWidgetChange(layoutContextValue.widget.state);
32 }
33 }, [onWidgetChange, layoutContextValue.widget.state]);
34
35 return <LayoutContext.Provider value={layoutContextValue}>{children}</LayoutContext.Provider>;
36}
Added view/app/src/vendor/livekit/react/components/participant/AudioTrack.tsx view/app/src/vendor/livekit/react/components/participant/AudioTrack.tsx +89 −0 View view/app/src/vendor/livekit/react/components/participant/AudioTrack.tsx
@@ -0,0 +1,89 @@
1import * as React from 'react';
2import { useMediaTrackBySourceOrName } from '../../hooks/useMediaTrackBySourceOrName';
3import type { TrackReference } from '@livekit/components-core';
4import { log } from '@livekit/components-core';
5import { RemoteAudioTrack, RemoteTrackPublication } from 'livekit-client';
6import { useEnsureTrackRef } from '../../context';
7
8/** @public */
9export interface AudioTrackProps extends React.AudioHTMLAttributes<HTMLAudioElement> {
10 /** The track reference of the track from which the audio is to be rendered. */
11 trackRef?: TrackReference;
12
13 onSubscriptionStatusChanged?: (subscribed: boolean) => void;
14 /** Sets the volume of the audio track. By default, the range is between `0.0` and `1.0`. */
15 volume?: number;
16 /**
17 * Mutes the audio track if set to `true`.
18 * @remarks
19 * If set to `true`, the server will stop sending audio track data to the client.
20 * @alpha
21 */
22 muted?: boolean;
23}
24
25/**
26 * The AudioTrack component is responsible for rendering participant audio tracks.
27 * This component must have access to the participant's context, or alternatively pass it a `Participant` as a property.
28 *
29 * @example
30 * ```tsx
31 * <ParticipantTile>
32 * <AudioTrack trackRef={trackRef} />
33 * </ParticipantTile>
34 * ```
35 *
36 * @see `ParticipantTile` component
37 * @public
38 */
39export const AudioTrack: (
40 props: AudioTrackProps & React.RefAttributes<HTMLAudioElement>,
41) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLAudioElement, AudioTrackProps>(
42 function AudioTrack(
43 { trackRef, onSubscriptionStatusChanged, volume, ...props }: AudioTrackProps,
44 ref,
45 ) {
46 const trackReference = useEnsureTrackRef(trackRef);
47
48 const mediaEl = React.useRef<HTMLAudioElement>(null);
49 React.useImperativeHandle(ref, () => mediaEl.current as HTMLAudioElement);
50
51 const {
52 elementProps,
53 isSubscribed,
54 track,
55 publication: pub,
56 } = useMediaTrackBySourceOrName(trackReference, {
57 element: mediaEl,
58 props,
59 });
60
61 React.useEffect(() => {
62 onSubscriptionStatusChanged?.(!!isSubscribed);
63 }, [isSubscribed, onSubscriptionStatusChanged]);
64
65 React.useEffect(() => {
66 if (track === undefined || volume === undefined) {
67 return;
68 }
69 if (track instanceof RemoteAudioTrack) {
70 track.setVolume(volume);
71 } else {
72 log.warn('Volume can only be set on remote audio tracks.');
73 }
74 }, [volume, track]);
75
76 React.useEffect(() => {
77 if (pub === undefined || props.muted === undefined) {
78 return;
79 }
80 if (pub instanceof RemoteTrackPublication) {
81 pub.setEnabled(!props.muted);
82 } else {
83 log.warn('Can only call setEnabled on remote track publications.');
84 }
85 }, [props.muted, pub, track]);
86
87 return <audio ref={mediaEl} {...elementProps} />;
88 },
89);
Added view/app/src/vendor/livekit/react/components/participant/ConnectionQualityIndicator.tsx view/app/src/vendor/livekit/react/components/participant/ConnectionQualityIndicator.tsx +55 −0 View view/app/src/vendor/livekit/react/components/participant/ConnectionQualityIndicator.tsx
@@ -0,0 +1,55 @@
1import * as React from 'react';
2import { mergeProps } from '../../utils';
3import { getConnectionQualityIcon } from '../../assets/icons/util';
4import type { ConnectionQualityIndicatorOptions } from '../../hooks';
5import { useConnectionQualityIndicator } from '../../hooks';
6import { lkT } from '@app/vendor/livekit/lk-i18n';
7import type { MessageKey } from '@app/i18n';
8
9/**
10 * Local modification: the official indicator is icon-only, so the connection
11 * state was invisible to a screen reader and had no tooltip. The label follows
12 * the observed value, not a one-off read, so it stays correct while it changes.
13 */
14const QUALITY_KEYS: Record<string, MessageKey> = {
15 excellent: 'chat.voice.qualityHigh',
16 good: 'chat.voice.qualityMedium',
17 poor: 'chat.voice.qualityLow',
18 unknown: 'chat.voice.qualityUnknown',
19};
20
21/** @public */
22export interface ConnectionQualityIndicatorProps
23 extends React.HTMLAttributes<HTMLDivElement>, ConnectionQualityIndicatorOptions {}
24
25/**
26 * The `ConnectionQualityIndicator` shows the individual connection quality of a participant.
27 *
28 * @example
29 * ```tsx
30 * <ConnectionQualityIndicator />
31 * ```
32 * @public
33 */
34export const ConnectionQualityIndicator: (
35 props: ConnectionQualityIndicatorProps & React.RefAttributes<HTMLDivElement>,
36) => React.ReactNode = /* @__PURE__ */ React.forwardRef<
37 HTMLDivElement,
38 ConnectionQualityIndicatorProps
39>(function ConnectionQualityIndicator(props: ConnectionQualityIndicatorProps, ref) {
40 const { className, quality } = useConnectionQualityIndicator(props);
41 const elementProps = React.useMemo(() => {
42 const label = lkT(QUALITY_KEYS[quality] ?? 'chat.voice.qualityUnknown');
43 return {
44 ...mergeProps(props, { className: className as string }),
45 'data-lk-quality': quality,
46 title: label,
47 'aria-label': label,
48 };
49 }, [quality, props, className]);
50 return (
51 <div ref={ref} {...elementProps}>
52 {props.children ?? getConnectionQualityIcon(quality)}
53 </div>
54 );
55});
Added view/app/src/vendor/livekit/react/components/participant/ParticipantName.tsx view/app/src/vendor/livekit/react/components/participant/ParticipantName.tsx +49 −0 View view/app/src/vendor/livekit/react/components/participant/ParticipantName.tsx
@@ -0,0 +1,49 @@
1import { setupParticipantName } from '@livekit/components-core';
2import * as React from 'react';
3import { useEnsureParticipant } from '../../context';
4import { useObservableState } from '../../hooks/internal/useObservableState';
5import { mergeProps } from '../../utils';
6import type { UseParticipantInfoOptions } from '../../hooks';
7
8/** @public */
9export interface ParticipantNameProps
10 extends React.HTMLAttributes<HTMLSpanElement>, UseParticipantInfoOptions {}
11
12/**
13 * The `ParticipantName` component displays the name of the participant as a string within an HTML span element.
14 * If no participant name is undefined the participant identity string is displayed.
15 *
16 * @example
17 * ```tsx
18 * <ParticipantName />
19 * ```
20 * @public
21 */
22export const ParticipantName: (
23 props: ParticipantNameProps & React.RefAttributes<HTMLSpanElement>,
24) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLSpanElement, ParticipantNameProps>(
25 function ParticipantName({ participant, ...props }: ParticipantNameProps, ref) {
26 const p = useEnsureParticipant(participant);
27
28 const { className, infoObserver } = React.useMemo(() => {
29 return setupParticipantName(p);
30 }, [p]);
31
32 const { identity, name } = useObservableState(infoObserver, {
33 name: p.name,
34 identity: p.identity,
35 metadata: p.metadata,
36 });
37
38 const mergedProps = React.useMemo(() => {
39 return mergeProps(props, { className, 'data-lk-participant-name': name });
40 }, [props, className, name]);
41
42 return (
43 <span ref={ref} {...mergedProps}>
44 {name !== '' ? name : identity}
45 {props.children}
46 </span>
47 );
48 },
49);
Added view/app/src/vendor/livekit/react/components/participant/ParticipantTile.tsx view/app/src/vendor/livekit/react/components/participant/ParticipantTile.tsx +193 −0 View view/app/src/vendor/livekit/react/components/participant/ParticipantTile.tsx
@@ -0,0 +1,193 @@
1import * as React from 'react';
2import type { Participant } from 'livekit-client';
3import { Track } from 'livekit-client';
4import type { ParticipantClickEvent, TrackReferenceOrPlaceholder } from '@livekit/components-core';
5import { isTrackReference, isTrackReferencePinned } from '@livekit/components-core';
6import { ConnectionQualityIndicator } from './ConnectionQualityIndicator';
7import { ParticipantName } from './ParticipantName';
8import { TrackMutedIndicator } from './TrackMutedIndicator';
9import {
10 ParticipantContext,
11 TrackRefContext,
12 useEnsureTrackRef,
13 useFeatureContext,
14 useMaybeLayoutContext,
15 useMaybeParticipantContext,
16 useMaybeTrackRefContext,
17} from '../../context';
18import { FocusToggle } from '../controls/FocusToggle';
19import { ParticipantPlaceholder } from '../../assets/images';
20import { LockLockedIcon, ScreenShareIcon } from '../../assets/icons';
21import { VideoTrack } from './VideoTrack';
22import { AudioTrack } from './AudioTrack';
23import { useParticipantTile } from '../../hooks';
24import { useIsEncrypted } from '../../hooks/useIsEncrypted';
25import { lkT } from '@app/vendor/livekit/lk-i18n';
26
27/**
28 * The `ParticipantContextIfNeeded` component only creates a `ParticipantContext`
29 * if there is no `ParticipantContext` already.
30 * @example
31 * ```tsx
32 * <ParticipantContextIfNeeded participant={trackReference.participant}>
33 * ...
34 * </ParticipantContextIfNeeded>
35 * ```
36 * @public
37 */
38export function ParticipantContextIfNeeded(
39 props: React.PropsWithChildren<{
40 participant?: Participant;
41 }>,
42) {
43 const hasContext = !!useMaybeParticipantContext();
44 return props.participant && !hasContext ? (
45 <ParticipantContext.Provider value={props.participant}>
46 {props.children}
47 </ParticipantContext.Provider>
48 ) : (
49 <>{props.children}</>
50 );
51}
52
53/**
54 * Only create a `TrackRefContext` if there is no `TrackRefContext` already.
55 * @internal
56 */
57export function TrackRefContextIfNeeded(
58 props: React.PropsWithChildren<{
59 trackRef?: TrackReferenceOrPlaceholder;
60 }>,
61) {
62 const hasContext = !!useMaybeTrackRefContext();
63 return props.trackRef && !hasContext ? (
64 <TrackRefContext.Provider value={props.trackRef}>{props.children}</TrackRefContext.Provider>
65 ) : (
66 <>{props.children}</>
67 );
68}
69
70/** @public */
71export interface ParticipantTileProps extends React.HTMLAttributes<HTMLDivElement> {
72 /** The track reference to display. */
73 trackRef?: TrackReferenceOrPlaceholder;
74 disableSpeakingIndicator?: boolean;
75
76 onParticipantClick?: (event: ParticipantClickEvent) => void;
77}
78
79/**
80 * The `ParticipantTile` component is the base utility wrapper for displaying a visual representation of a participant.
81 * This component can be used as a child of the `TrackLoop` component or by passing a track reference as property.
82 *
83 * @example Using the `ParticipantTile` component with a track reference:
84 * ```tsx
85 * <ParticipantTile trackRef={trackRef} />
86 * ```
87 * @example Using the `ParticipantTile` component as a child of the `TrackLoop` component:
88 * ```tsx
89 * <TrackLoop>
90 * <ParticipantTile />
91 * </TrackLoop>
92 * ```
93 * @public
94 */
95export const ParticipantTile: (
96 props: ParticipantTileProps & React.RefAttributes<HTMLDivElement>,
97) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLDivElement, ParticipantTileProps>(
98 function ParticipantTile(
99 {
100 trackRef,
101 children,
102 onParticipantClick,
103 disableSpeakingIndicator,
104 ...htmlProps
105 }: ParticipantTileProps,
106 ref,
107 ) {
108 const trackReference = useEnsureTrackRef(trackRef);
109
110 const { elementProps } = useParticipantTile<HTMLDivElement>({
111 htmlProps,
112 disableSpeakingIndicator,
113 onParticipantClick,
114 trackRef: trackReference,
115 });
116 const isEncrypted = useIsEncrypted(trackReference.participant);
117 const layoutContext = useMaybeLayoutContext();
118
119 const autoManageSubscription = useFeatureContext()?.autoSubscription;
120
121 const handleSubscribe = React.useCallback(
122 (subscribed: boolean) => {
123 if (
124 trackReference.source &&
125 !subscribed &&
126 layoutContext &&
127 layoutContext.pin.dispatch &&
128 isTrackReferencePinned(trackReference, layoutContext.pin.state)
129 ) {
130 layoutContext.pin.dispatch({ msg: 'clear_pin' });
131 }
132 },
133 [trackReference, layoutContext],
134 );
135
136 return (
137 <div ref={ref} style={{ position: 'relative' }} {...elementProps}>
138 <TrackRefContextIfNeeded trackRef={trackReference}>
139 <ParticipantContextIfNeeded participant={trackReference.participant}>
140 {children ?? (
141 <>
142 {isTrackReference(trackReference) &&
143 (trackReference.publication?.kind === 'video' ||
144 trackReference.source === Track.Source.Camera ||
145 trackReference.source === Track.Source.ScreenShare) ? (
146 <VideoTrack
147 trackRef={trackReference}
148 onSubscriptionStatusChanged={handleSubscribe}
149 manageSubscription={autoManageSubscription}
150 />
151 ) : (
152 isTrackReference(trackReference) && (
153 <AudioTrack
154 trackRef={trackReference}
155 onSubscriptionStatusChanged={handleSubscribe}
156 />
157 )
158 )}
159 <div className="lk-participant-placeholder">
160 <ParticipantPlaceholder />
161 </div>
162 <div className="lk-participant-metadata">
163 <div className="lk-participant-metadata-item">
164 {trackReference.source === Track.Source.Camera ? (
165 <>
166 {isEncrypted && <LockLockedIcon style={{ marginRight: '0.25rem' }} />}
167 <TrackMutedIndicator
168 trackRef={{
169 participant: trackReference.participant,
170 source: Track.Source.Microphone,
171 }}
172 show={'muted'}
173 ></TrackMutedIndicator>
174 <ParticipantName />
175 </>
176 ) : (
177 <>
178 <ScreenShareIcon style={{ marginRight: '0.25rem' }} />
179 <ParticipantName>{lkT('livekit.screenSuffix')}</ParticipantName>
180 </>
181 )}
182 </div>
183 <ConnectionQualityIndicator className="lk-participant-metadata-item" />
184 </div>
185 </>
186 )}
187 <FocusToggle trackRef={trackReference} />
188 </ParticipantContextIfNeeded>
189 </TrackRefContextIfNeeded>
190 </div>
191 );
192 },
193);
Added view/app/src/vendor/livekit/react/components/participant/TrackMutedIndicator.tsx view/app/src/vendor/livekit/react/components/participant/TrackMutedIndicator.tsx +53 −0 View view/app/src/vendor/livekit/react/components/participant/TrackMutedIndicator.tsx
@@ -0,0 +1,53 @@
1import * as React from 'react';
2import { mergeProps } from '../../utils';
3import { getSourceIcon } from '../../assets/icons/util';
4import { useTrackMutedIndicator } from '../../hooks';
5import type { TrackReferenceOrPlaceholder } from '@livekit/components-core';
6
7/** @public */
8export interface TrackMutedIndicatorProps extends React.HTMLAttributes<HTMLDivElement> {
9 trackRef: TrackReferenceOrPlaceholder;
10 show?: 'always' | 'muted' | 'unmuted';
11}
12
13/**
14 * The `TrackMutedIndicator` shows whether the participant's camera or microphone is muted or not.
15 * By default, a muted/unmuted icon is displayed for a camera, microphone, and screen sharing track.
16 *
17 * @example
18 * ```tsx
19 * <TrackMutedIndicator trackRef={trackRef} />
20 * ```
21 * @public
22 */
23export const TrackMutedIndicator: (
24 props: TrackMutedIndicatorProps & React.RefAttributes<HTMLDivElement>,
25) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLDivElement, TrackMutedIndicatorProps>(
26 function TrackMutedIndicator(
27 { trackRef, show = 'always', ...props }: TrackMutedIndicatorProps,
28 ref,
29 ) {
30 const { className, isMuted } = useTrackMutedIndicator(trackRef);
31
32 const showIndicator =
33 show === 'always' || (show === 'muted' && isMuted) || (show === 'unmuted' && !isMuted);
34
35 const htmlProps = React.useMemo(
36 () =>
37 mergeProps(props, {
38 className,
39 }),
40 [className, props],
41 );
42
43 if (!showIndicator) {
44 return null;
45 }
46
47 return (
48 <div ref={ref} {...htmlProps} data-lk-muted={isMuted}>
49 {props.children ?? getSourceIcon(trackRef.source, !isMuted)}
50 </div>
51 );
52 },
53);
Added view/app/src/vendor/livekit/react/components/participant/VideoTrack.tsx view/app/src/vendor/livekit/react/components/participant/VideoTrack.tsx +121 −0 View view/app/src/vendor/livekit/react/components/participant/VideoTrack.tsx
@@ -0,0 +1,121 @@
1import { RemoteTrackPublication } from 'livekit-client';
2import * as React from 'react';
3import { useMediaTrackBySourceOrName } from '../../hooks/useMediaTrackBySourceOrName';
4import type { ParticipantClickEvent, TrackReference } from '@livekit/components-core';
5import { useEnsureTrackRef } from '../../context';
6import * as useHooks from 'usehooks-ts';
7
8/** @public */
9export interface VideoTrackProps extends React.VideoHTMLAttributes<HTMLVideoElement> {
10 /** The track reference of the track to render. */
11 trackRef?: TrackReference;
12 onTrackClick?: (evt: ParticipantClickEvent) => void;
13 onSubscriptionStatusChanged?: (subscribed: boolean) => void;
14 manageSubscription?: boolean;
15}
16
17/**
18 * The `VideoTrack` component is responsible for rendering participant video tracks like `camera` and `screen_share`.
19 * This component must have access to the participant's context, or alternatively pass it a `Participant` as a property.
20 *
21 * @example
22 * ```tsx
23 * <VideoTrack trackRef={trackRef} />
24 * ```
25 * @see {@link @livekit/components-react#ParticipantTile | ParticipantTile}
26 * @public
27 */
28export const VideoTrack: (
29 props: VideoTrackProps & React.RefAttributes<HTMLVideoElement>,
30) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLVideoElement, VideoTrackProps>(
31 function VideoTrack(
32 {
33 onTrackClick,
34 onClick,
35 onSubscriptionStatusChanged,
36 trackRef,
37 manageSubscription,
38 ...props
39 }: VideoTrackProps,
40 ref,
41 ) {
42 const trackReference = useEnsureTrackRef(trackRef);
43
44 const mediaEl = React.useRef<HTMLVideoElement | null>(null);
45 React.useImperativeHandle(ref, () => mediaEl.current as HTMLVideoElement);
46
47 const { ref: intersectionRef, entry: intersectionEntry } = useHooks.useIntersectionObserver({
48 root: null,
49 });
50
51 const [debouncedIntersectionEntry] = useHooks.useDebounceValue(intersectionEntry, 3000);
52
53 const managedPublicationRef = React.useRef<RemoteTrackPublication | null>(null);
54 const manageSubscriptionRef = React.useRef(false);
55
56 React.useEffect(() => {
57 manageSubscriptionRef.current = Boolean(manageSubscription);
58 managedPublicationRef.current =
59 trackReference.publication instanceof RemoteTrackPublication
60 ? trackReference.publication
61 : null;
62 }, [trackReference.publication, manageSubscription]);
63
64 React.useEffect(() => {
65 if (
66 manageSubscription &&
67 trackReference.publication instanceof RemoteTrackPublication &&
68 debouncedIntersectionEntry?.isIntersecting === false &&
69 intersectionEntry?.isIntersecting === false
70 ) {
71 trackReference.publication.setSubscribed(false);
72 }
73 }, [debouncedIntersectionEntry, intersectionEntry, trackReference, manageSubscription]);
74
75 React.useEffect(() => {
76 if (
77 manageSubscription &&
78 trackReference.publication instanceof RemoteTrackPublication &&
79 intersectionEntry?.isIntersecting === true
80 ) {
81 trackReference.publication.setSubscribed(true);
82 }
83 }, [intersectionEntry, trackReference, manageSubscription]);
84
85 React.useEffect(() => {
86 return () => {
87 if (manageSubscriptionRef.current) {
88 managedPublicationRef.current?.setSubscribed(false);
89 }
90 };
91 }, []);
92
93 const {
94 elementProps,
95 publication: pub,
96 isSubscribed,
97 } = useMediaTrackBySourceOrName(trackReference, {
98 element: mediaEl,
99 props,
100 });
101
102 React.useEffect(() => {
103 onSubscriptionStatusChanged?.(!!isSubscribed);
104 }, [isSubscribed, onSubscriptionStatusChanged]);
105
106 const clickHandler = (evt: React.MouseEvent<HTMLVideoElement, MouseEvent>) => {
107 onClick?.(evt);
108 onTrackClick?.({ participant: trackReference?.participant, track: pub });
109 };
110
111 const setVideoRef = React.useCallback(
112 (el: HTMLVideoElement | null) => {
113 mediaEl.current = el;
114 intersectionRef(el);
115 },
116 [intersectionRef],
117 );
118
119 return <video ref={setVideoRef} {...elementProps} muted={true} onClick={clickHandler}></video>;
120 },
121);
Added view/app/src/vendor/livekit/react/components/ChatEntry.tsx view/app/src/vendor/livekit/react/components/ChatEntry.tsx +112 −0 View view/app/src/vendor/livekit/react/components/ChatEntry.tsx
@@ -0,0 +1,112 @@
1import type { ReceivedChatMessage } from '@livekit/components-core';
2import { tokenize, createDefaultGrammar } from '@livekit/components-core';
3import * as React from 'react';
4
5/** @public */
6export type MessageFormatter = (message: string) => React.ReactNode;
7
8/**
9 * ChatEntry composes the HTML div element under the hood, so you can pass all its props.
10 * These are the props specific to the ChatEntry component:
11 * @public
12 */
13export interface ChatEntryProps extends React.HTMLAttributes<HTMLLIElement> {
14 /** The chat massage object to display. */
15 entry: ReceivedChatMessage;
16 /** Hide sender name. Useful when displaying multiple consecutive chat messages from the same person. */
17 hideName?: boolean;
18 /** Hide message timestamp. */
19 hideTimestamp?: boolean;
20 /** An optional formatter for the message body. */
21 messageFormatter?: MessageFormatter;
22}
23
24/**
25 * The `ChatEntry` component holds and displays one chat message.
26 *
27 * @example
28 * ```tsx
29 * <Chat>
30 * <ChatEntry />
31 * </Chat>
32 * ```
33 * @see `Chat`
34 * @public
35 */
36export const ChatEntry: (
37 props: ChatEntryProps & React.RefAttributes<HTMLLIElement>,
38) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLLIElement, ChatEntryProps>(
39 function ChatEntry(
40 { entry, hideName = false, hideTimestamp = false, messageFormatter, ...props }: ChatEntryProps,
41 ref,
42 ) {
43 const formattedMessage = React.useMemo(() => {
44 return messageFormatter ? messageFormatter(entry.message) : entry.message;
45 }, [entry.message, messageFormatter]);
46 const hasBeenEdited = !!entry.editTimestamp;
47 const time = new Date(entry.timestamp);
48 const locale = typeof navigator !== 'undefined' ? navigator.language : 'en-US';
49
50 const name = entry.from?.name ?? entry.from?.identity;
51
52 return (
53 <li
54 ref={ref}
55 className="lk-chat-entry"
56 title={time.toLocaleTimeString(locale, { timeStyle: 'full' })}
57 data-lk-message-origin={entry.from?.isLocal ? 'local' : 'remote'}
58 {...props}
59 >
60 {(!hideTimestamp || !hideName || hasBeenEdited) && (
61 <span className="lk-meta-data">
62 {!hideName && <strong className="lk-participant-name">{name}</strong>}
63
64 {(!hideTimestamp || hasBeenEdited) && (
65 <span className="lk-timestamp">
66 {hasBeenEdited && 'edited '}
67 {time.toLocaleTimeString(locale, { timeStyle: 'short' })}
68 </span>
69 )}
70 </span>
71 )}
72
73 <span className="lk-message-body">{formattedMessage}</span>
74 <span className="lk-message-attachements">
75 {entry.attachedFiles?.map(
76 (file) =>
77 file.type.startsWith('image/') && (
78 <img
79 style={{ maxWidth: '300px', maxHeight: '300px' }}
80 key={file.name}
81 src={URL.createObjectURL(file)}
82 alt={file.name}
83 />
84 ),
85 )}
86 </span>
87 </li>
88 );
89 },
90);
91
92/** @public */
93export function formatChatMessageLinks(message: string): React.ReactNode {
94 return tokenize(message, createDefaultGrammar()).map((tok, i) => {
95 if (typeof tok === `string`) {
96 return tok;
97 } else {
98 const content = tok.content.toString();
99 const href =
100 tok.type === `url`
101 ? /^http(s?):\/\//.test(content)
102 ? content
103 : `https://${content}`
104 : `mailto:${content}`;
105 return (
106 <a className="lk-chat-link" key={i} href={href} target="_blank" rel="noreferrer">
107 {content}
108 </a>
109 );
110 }
111 });
112}
Added view/app/src/vendor/livekit/react/components/ConnectionState.tsx view/app/src/vendor/livekit/react/components/ConnectionState.tsx +36 −0 View view/app/src/vendor/livekit/react/components/ConnectionState.tsx
@@ -0,0 +1,36 @@
1import type { Room } from 'livekit-client';
2import * as React from 'react';
3import { useConnectionState } from '../hooks';
4
5/** @public */
6export interface ConnectionStatusProps extends React.HTMLAttributes<HTMLDivElement> {
7 /**
8 * The room from which the connection status should be displayed.
9 */
10 room?: Room;
11}
12
13/**
14 * The `ConnectionState` component displays the connection status of the room as strings
15 * (`"connected" | "connecting" | "disconnected" | "reconnecting"`).
16 *
17 * @example
18 * ```tsx
19 * <LiveKitRoom>
20 * <ConnectionState />
21 * </LiveKitRoom>
22 * ```
23 * @public
24 */
25export const ConnectionState: (
26 props: ConnectionStatusProps & React.RefAttributes<HTMLDivElement>,
27) => React.ReactNode = /* @__PURE__ */ React.forwardRef<HTMLDivElement, ConnectionStatusProps>(
28 function ConnectionState({ room, ...props }: ConnectionStatusProps, ref) {
29 const connectionState = useConnectionState(room);
30 return (
31 <div ref={ref} {...props}>
32 {connectionState}
33 </div>
34 );
35 },
36);
Added view/app/src/vendor/livekit/react/components/ConnectionStateToast.tsx view/app/src/vendor/livekit/react/components/ConnectionStateToast.tsx +48 −0 View view/app/src/vendor/livekit/react/components/ConnectionStateToast.tsx
@@ -0,0 +1,48 @@
1import type { Room } from 'livekit-client';
2import { ConnectionState } from 'livekit-client';
3import * as React from 'react';
4import { SpinnerIcon } from '../assets/icons';
5import { useConnectionState } from '../hooks';
6import { Toast } from './Toast';
7import { lkT } from '@app/vendor/livekit/lk-i18n';
8
9/** @public */
10export interface ConnectionStateToastProps extends React.HTMLAttributes<HTMLDivElement> {
11 room?: Room;
12}
13
14/**
15 * The `ConnectionStateToast` component displays a toast
16 * notification indicating the current connection state of the room.
17 * @public
18 */
19export function ConnectionStateToast(props: ConnectionStateToastProps) {
20 const [notification, setNotification] = React.useState<React.ReactElement | undefined>(undefined);
21 const state = useConnectionState(props.room);
22
23 React.useEffect(() => {
24 switch (state) {
25 case ConnectionState.Reconnecting:
26 setNotification(
27 <>
28 <SpinnerIcon className="lk-spinner" /> {lkT("livekit.reconnecting")}
29 </>,
30 );
31 break;
32 case ConnectionState.Connecting:
33 setNotification(
34 <>
35 <SpinnerIcon className="lk-spinner" /> {lkT("livekit.connecting")}
36 </>,
37 );
38 break;
39 case ConnectionState.Disconnected:
40 setNotification(<>{lkT("livekit.disconnected")}</>);
41 break;
42 default:
43 setNotification(undefined);
44 break;
45 }
46 }, [state]);
47 return notification ? <Toast className="lk-toast-connection-state">{notification}</Toast> : <></>;
48}
Added view/app/src/vendor/livekit/react/components/index.ts view/app/src/vendor/livekit/react/components/index.ts +30 −0 View view/app/src/vendor/livekit/react/components/index.ts
@@ -0,0 +1,30 @@
1export * from './controls/ClearPinButton';
2export * from './ConnectionState';
3export * from './controls/ChatToggle';
4export * from './controls/DisconnectButton';
5export * from './controls/FocusToggle';
6export * from './controls/MediaDeviceSelect';
7export * from './controls/StartAudio';
8export * from './controls/StartMediaButton';
9export * from './controls/TrackToggle';
10export * from './layout';
11export * from './layout/LayoutContextProvider';
12export * from './LiveKitRoom';
13export * from './participant/ConnectionQualityIndicator';
14export * from './participant/AudioTrack';
15export * from './participant/VideoTrack';
16export * from './participant/ParticipantName';
17export * from './participant/TrackMutedIndicator';
18export * from './ParticipantLoop';
19export { RoomAudioRenderer, type RoomAudioRendererProps } from './RoomAudioRenderer';
20export * from './RoomName';
21export { Toast } from './Toast';
22export * from './TrackLoop';
23export * from './participant/ParticipantTile';
24export { ConnectionStateToast, type ConnectionStateToastProps } from './ConnectionStateToast';
25export {
26 type MessageFormatter,
27 type ChatEntryProps,
28 ChatEntry,
29 formatChatMessageLinks,
30} from './ChatEntry';
Added view/app/src/vendor/livekit/react/components/LiveKitRoom.tsx view/app/src/vendor/livekit/react/components/LiveKitRoom.tsx +122 −0 View view/app/src/vendor/livekit/react/components/LiveKitRoom.tsx
@@ -0,0 +1,122 @@
1import type {
2 AudioCaptureOptions,
3 DisconnectReason,
4 RoomConnectOptions,
5 RoomOptions,
6 ScreenShareCaptureOptions,
7 VideoCaptureOptions,
8} from 'livekit-client';
9import type { MediaDeviceFailure, Room } from 'livekit-client';
10import * as React from 'react';
11import { type FeatureFlags, LKFeatureContext, RoomContext } from '../context';
12import { useLiveKitRoom } from '../hooks';
13
14/** @public */
15export interface LiveKitRoomProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onError'> {
16 /**
17 * URL to the LiveKit server.
18 * For example: `wss://<domain>.livekit.cloud`
19 * To simplify the implementation, `undefined` is also accepted as an intermediate value, but only with a valid string url can the connection be established.
20 */
21 serverUrl: string | undefined;
22 /**
23 * A user specific access token for a client to authenticate to the room.
24 * This token is necessary to establish a connection to the room.
25 * To simplify the implementation, `undefined` is also accepted as an intermediate value, but only with a valid string token can the connection be established.
26 *
27 * @see https://docs.livekit.io/cloud/project-management/keys-and-tokens/#generating-access-tokens
28 */
29 token: string | undefined;
30 /**
31 * Publish audio immediately after connecting to your LiveKit room.
32 * @defaultValue `false`
33 * @see https://docs.livekit.io/client-sdk-js/interfaces/AudioCaptureOptions.html
34 */
35 audio?: AudioCaptureOptions | boolean;
36 /**
37 * Publish video immediately after connecting to your LiveKit room.
38 * @defaultValue `false`
39 * @see https://docs.livekit.io/client-sdk-js/interfaces/VideoCaptureOptions.html
40 */
41 video?: VideoCaptureOptions | boolean;
42 /**
43 * Publish screen share immediately after connecting to your LiveKit room.
44 * @defaultValue `false`
45 * @see https://docs.livekit.io/client-sdk-js/interfaces/ScreenShareCaptureOptions.html
46 */
47 screen?: ScreenShareCaptureOptions | boolean;
48 /**
49 * If set to true a connection to LiveKit room is initiated.
50 * @defaultValue `true`
51 */
52 connect?: boolean;
53 /**
54 * Options for when creating a new room.
55 * When you pass your own room instance to this component, these options have no effect.
56 * Instead, set the options directly in the room instance.
57 *
58 * @see https://docs.livekit.io/client-sdk-js/interfaces/RoomOptions.html
59 */
60 options?: RoomOptions;
61 /**
62 * Define options how to connect to the LiveKit server.
63 *
64 * @see https://docs.livekit.io/client-sdk-js/interfaces/RoomConnectOptions.html
65 */
66 connectOptions?: RoomConnectOptions;
67 onConnected?: () => void;
68 onDisconnected?: (reason?: DisconnectReason) => void;
69 onError?: (error: Error) => void;
70 onMediaDeviceFailure?: (failure?: MediaDeviceFailure, kind?: MediaDeviceKind) => void;
71 onEncryptionError?: (error: Error) => void;
72 /**
73 * Optional room instance.
74 * By passing your own room instance you overwrite the `options` parameter,
75 * make sure to set the options directly on the room instance itself.
76 */
77 room?: Room;
78
79 simulateParticipants?: number | undefined;
80
81 /**
82 * @internal
83 */
84 featureFlags?: FeatureFlags;
85}
86
87/**
88 * The `LiveKitRoom` component provides the room context to all its child components.
89 * It is generally the starting point of your LiveKit app and the root of the LiveKit component tree.
90 * It provides the room state as a React context to all child components, so you don't have to pass it yourself.
91 *
92 * @example
93 * ```tsx
94 * <LiveKitRoom
95 * token='<livekit-token>'
96 * serverUrl='<url-to-livekit-server>'
97 * connect={true}
98 * >
99 * ...
100 * </LiveKitRoom>
101 * ```
102 * @public
103 */
104export const LiveKitRoom: (
105 props: React.PropsWithChildren<LiveKitRoomProps> & React.RefAttributes<HTMLDivElement>,
106) => React.ReactNode = /* @__PURE__ */ React.forwardRef<
107 HTMLDivElement,
108 React.PropsWithChildren<LiveKitRoomProps>
109>(function LiveKitRoom(props: React.PropsWithChildren<LiveKitRoomProps>, ref) {
110 const { room, htmlProps } = useLiveKitRoom(props);
111 return (
112 <div ref={ref} {...htmlProps}>
113 {room && (
114 <RoomContext.Provider value={room}>
115 <LKFeatureContext.Provider value={props.featureFlags}>
116 {props.children}
117 </LKFeatureContext.Provider>
118 </RoomContext.Provider>
119 )}
120 </div>
121 );
122});
Added view/app/src/vendor/livekit/react/components/ParticipantLoop.tsx view/app/src/vendor/livekit/react/components/ParticipantLoop.tsx +41 −0 View view/app/src/vendor/livekit/react/components/ParticipantLoop.tsx
@@ -0,0 +1,41 @@
1import type { Participant } from 'livekit-client';
2import * as React from 'react';
3import { ParticipantContext } from '../context';
4import { cloneSingleChild } from '../utils';
5
6/** @public */
7export interface ParticipantLoopProps {
8 /** The participants to loop over. Use `useParticipants()` hook to get participants. */
9 participants: Participant[];
10 /** The template component to be used in the loop. */
11 children: React.ReactNode;
12}
13
14/**
15 * The `ParticipantLoop` component loops over an array of participants to create a context for every participant.
16 * This component takes exactly one child component as a template.
17 * By providing your own template as a child you have full control over the look and feel of your participant representations.
18 *
19 * @remarks
20 * If you want to loop over individual tracks instead of participants, you can use the `TrackLoop` component.
21 *
22 * @example
23 * ```tsx
24 * const participants = useParticipants();
25 * <ParticipantLoop participants={participants}>
26 * <ParticipantName />
27 * </ParticipantLoop>
28 * ```
29 * @public
30 */
31export function ParticipantLoop({ participants, ...props }: ParticipantLoopProps) {
32 return (
33 <>
34 {participants.map((participant) => (
35 <ParticipantContext.Provider value={participant} key={participant.identity}>
36 {cloneSingleChild(props.children)}
37 </ParticipantContext.Provider>
38 ))}
39 </>
40 );
41}
Added view/app/src/vendor/livekit/react/components/RoomAudioRenderer.tsx view/app/src/vendor/livekit/react/components/RoomAudioRenderer.tsx +56 −0 View view/app/src/vendor/livekit/react/components/RoomAudioRenderer.tsx
@@ -0,0 +1,56 @@
1import { getTrackReferenceId } from '@livekit/components-core';
2import { Room, Track } from 'livekit-client';
3import { useTracks } from '../hooks';
4import { AudioTrack } from './participant/AudioTrack';
5
6/** @public */
7export interface RoomAudioRendererProps {
8 room?: Room;
9
10 /** Sets the volume for all audio tracks rendered by this component. By default, the range is between `0.0` and `1.0`. */
11 volume?: number;
12
13 /**
14 * If set to `true`, mutes all audio tracks rendered by the component.
15 * @remarks
16 * If set to `true`, the server will stop sending audio track data to the client.
17 * @alpha
18 */
19 muted?: boolean;
20}
21
22/**
23 * The `RoomAudioRenderer` component is a drop-in solution for adding audio to your LiveKit app.
24 * It takes care of handling remote participants’ audio tracks and makes sure that microphones and screen share are audible.
25 *
26 * @example
27 * ```tsx
28 * <LiveKitRoom>
29 * <RoomAudioRenderer />
30 * </LiveKitRoom>
31 * ```
32 * @public
33 */
34export function RoomAudioRenderer({ room, volume, muted }: RoomAudioRendererProps) {
35 const tracks = useTracks(
36 [Track.Source.Microphone, Track.Source.ScreenShareAudio, Track.Source.Unknown],
37 {
38 updateOnlyOn: [],
39 onlySubscribed: true,
40 room,
41 },
42 ).filter((ref) => !ref.participant.isLocal && ref.publication.kind === Track.Kind.Audio);
43
44 return (
45 <div style={{ display: 'none' }}>
46 {tracks.map((trackRef) => (
47 <AudioTrack
48 key={getTrackReferenceId(trackRef)}
49 trackRef={trackRef}
50 volume={volume}
51 muted={muted}
52 />
53 ))}
54 </div>
55 );
56}
Added view/app/src/vendor/livekit/react/components/RoomName.tsx view/app/src/vendor/livekit/react/components/RoomName.tsx +36 −0 View view/app/src/vendor/livekit/react/components/RoomName.tsx
@@ -0,0 +1,36 @@
1import * as React from 'react';
2import { useRoomInfo } from '../hooks';
3
4/** @public */
5export interface RoomNameProps extends React.HTMLAttributes<HTMLSpanElement> {
6 childrenPosition?: 'before' | 'after';
7}
8
9/**
10 * The `RoomName` component renders the name of the connected LiveKit room inside a span tag.
11 *
12 * @example
13 * ```tsx
14 * <LiveKitRoom>
15 * <RoomName />
16 * </LiveKitRoom>
17 * ```
18 * @public
19 *
20 * @param props - RoomNameProps
21 */
22export const RoomName: React.FC<RoomNameProps & React.RefAttributes<HTMLSpanElement>> =
23 /* @__PURE__ */ React.forwardRef<HTMLSpanElement, RoomNameProps>(function RoomName(
24 { childrenPosition = 'before', children, ...htmlAttributes }: RoomNameProps,
25 ref,
26 ) {
27 const { name } = useRoomInfo();
28
29 return (
30 <span ref={ref} {...htmlAttributes}>
31 {childrenPosition === 'before' && children}
32 {name}
33 {childrenPosition === 'after' && children}
34 </span>
35 );
36 });
Added view/app/src/vendor/livekit/react/components/Toast.tsx view/app/src/vendor/livekit/react/components/Toast.tsx +18 −0 View view/app/src/vendor/livekit/react/components/Toast.tsx
@@ -0,0 +1,18 @@
1import * as React from 'react';
2import { mergeProps } from '../utils';
3
4/**
5 * The `Toast` component is a rudimentary way to display a message to the user.
6 * This message should be short lived and not require user interaction.
7 * For example, displaying the current connection state like `ConnectionStateToast` does.
8 *
9 * @example
10 * ```tsx
11 * <Toast>Connecting...</Toast>
12 * ```
13 * @public
14 */
15export function Toast(props: React.HTMLAttributes<HTMLDivElement>) {
16 const htmlProps = React.useMemo(() => mergeProps(props, { className: 'lk-toast' }), [props]);
17 return <div {...htmlProps}>{props.children}</div>;
18}
Added view/app/src/vendor/livekit/react/components/TrackLoop.tsx view/app/src/vendor/livekit/react/components/TrackLoop.tsx +45 −0 View view/app/src/vendor/livekit/react/components/TrackLoop.tsx
@@ -0,0 +1,45 @@
1import type { TrackReference, TrackReferenceOrPlaceholder } from '@livekit/components-core';
2import * as React from 'react';
3import { TrackRefContext } from '../context/track-reference-context';
4import { cloneSingleChild } from '../utils';
5import { getTrackReferenceId } from '@livekit/components-core';
6
7/** @public */
8export interface TrackLoopProps {
9 /** Track references to loop over. You can the use `useTracks()` hook to get TrackReferences. */
10 tracks: TrackReference[] | TrackReferenceOrPlaceholder[];
11 /** The template component to be used in the loop. */
12 children: React.ReactNode;
13}
14
15/**
16 * The `TrackLoop` component loops over tracks. It is for example a easy way to loop over all participant camera and screen share tracks.
17 * `TrackLoop` creates a `TrackRefContext` for each track that you can use to e.g. render the track.
18 *
19 * @example
20 * ```tsx
21 * const trackRefs = useTracks([Track.Source.Camera]);
22 * <TrackLoop tracks={trackRefs} >
23 * <TrackRefContext.Consumer>
24 * {(trackRef) => trackRef && <VideoTrack trackRef={trackRef}/>}
25 * </TrackRefContext.Consumer>
26 * </TrackLoop>
27 * ```
28 * @public
29 */
30export function TrackLoop({ tracks, ...props }: TrackLoopProps) {
31 return (
32 <>
33 {tracks.map((trackReference) => {
34 return (
35 <TrackRefContext.Provider
36 value={trackReference}
37 key={getTrackReferenceId(trackReference)}
38 >
39 {cloneSingleChild(props.children)}
40 </TrackRefContext.Provider>
41 );
42 })}
43 </>
44 );
45}
Added view/app/src/vendor/livekit/react/context/chat-context.ts view/app/src/vendor/livekit/react/context/chat-context.ts +37 −0 View view/app/src/vendor/livekit/react/context/chat-context.ts
@@ -0,0 +1,37 @@
1import type { WidgetState } from '@livekit/components-core';
2import type * as React from 'react';
3
4/** @internal */
5export type ChatContextAction =
6 | { msg: 'show_chat' }
7 | { msg: 'hide_chat' }
8 | { msg: 'toggle_chat' }
9 | { msg: 'unread_msg'; count: number }
10 | { msg: 'toggle_settings' };
11
12/** @internal */
13export type WidgetContextType = {
14 dispatch?: React.Dispatch<ChatContextAction>;
15 state?: WidgetState;
16};
17
18/** @internal */
19export function chatReducer(state: WidgetState, action: ChatContextAction): WidgetState {
20 if (action.msg === 'show_chat') {
21 return { ...state, showChat: true, unreadMessages: 0 };
22 } else if (action.msg === 'hide_chat') {
23 return { ...state, showChat: false };
24 } else if (action.msg === 'toggle_chat') {
25 const newState = { ...state, showChat: !state.showChat };
26 if (newState.showChat === true) {
27 newState.unreadMessages = 0;
28 }
29 return newState;
30 } else if (action.msg === 'unread_msg') {
31 return { ...state, unreadMessages: action.count };
32 } else if (action.msg === 'toggle_settings') {
33 return { ...state, showSettings: !state.showSettings };
34 } else {
35 return { ...state };
36 }
37}
Added view/app/src/vendor/livekit/react/context/feature-context.ts view/app/src/vendor/livekit/react/context/feature-context.ts +28 −0 View view/app/src/vendor/livekit/react/context/feature-context.ts
@@ -0,0 +1,28 @@
1import * as React from 'react';
2
3/** @internal */
4export interface FeatureFlags {
5 autoSubscription?: boolean;
6}
7
8type FeatureContext<T extends boolean = false> = T extends true
9 ? FeatureFlags
10 : FeatureFlags | undefined;
11
12/** @internal */
13export const LKFeatureContext = React.createContext<FeatureFlags | undefined>(undefined);
14
15/**
16 * @internal
17 */
18export function useFeatureContext<T extends boolean>(require?: T): FeatureContext<T> {
19 const ctx = React.useContext(LKFeatureContext) as FeatureContext<T>;
20 if (require === true) {
21 if (ctx) {
22 return ctx;
23 } else {
24 throw Error('tried to access feature context, but none is present');
25 }
26 }
27 return ctx;
28}
Added view/app/src/vendor/livekit/react/context/index.ts view/app/src/vendor/livekit/react/context/index.ts +26 −0 View view/app/src/vendor/livekit/react/context/index.ts
@@ -0,0 +1,26 @@
1export {} from './chat-context';
2export type { LayoutContextType } from './layout-context';
3export {
4 LayoutContext,
5 useCreateLayoutContext,
6 useEnsureCreateLayoutContext,
7 useEnsureLayoutContext,
8 useLayoutContext,
9 useMaybeLayoutContext,
10} from './layout-context';
11export {
12 ParticipantContext,
13 useEnsureParticipant,
14 useMaybeParticipantContext,
15 useParticipantContext,
16} from './participant-context';
17export {} from './pin-context';
18export { RoomContext, useEnsureRoom, useMaybeRoomContext, useRoomContext } from './room-context';
19export {
20 TrackRefContext,
21 useEnsureTrackRef,
22 useMaybeTrackRefContext,
23 useTrackRefContext,
24} from './track-reference-context';
25
26export { type FeatureFlags, useFeatureContext, LKFeatureContext } from './feature-context';
Added view/app/src/vendor/livekit/react/context/layout-context.ts view/app/src/vendor/livekit/react/context/layout-context.ts +72 −0 View view/app/src/vendor/livekit/react/context/layout-context.ts
@@ -0,0 +1,72 @@
1import { PIN_DEFAULT_STATE, WIDGET_DEFAULT_STATE } from '@livekit/components-core';
2import * as React from 'react';
3import type { PinContextType } from './pin-context';
4import type { WidgetContextType } from './chat-context';
5import { chatReducer } from './chat-context';
6import { pinReducer } from './pin-context';
7
8/** @public */
9export type LayoutContextType = {
10 pin: PinContextType;
11 widget: WidgetContextType;
12};
13
14/** @public */
15export const LayoutContext = React.createContext<LayoutContextType | undefined>(undefined);
16
17/**
18 * Ensures that a layout context is provided via context.
19 * If no layout context is provided, an error is thrown.
20 * @public
21 */
22export function useLayoutContext(): LayoutContextType {
23 const layoutContext = React.useContext(LayoutContext);
24 if (!layoutContext) {
25 throw Error('Tried to access LayoutContext context outside a LayoutContextProvider provider.');
26 }
27 return layoutContext;
28}
29
30/**
31 * Ensures that a layout context is provided, either via context or explicitly as a parameter.
32 * If not inside a `LayoutContext` and no layout context is provided, an error is thrown.
33 * @public
34 */
35export function useEnsureLayoutContext(layoutContext?: LayoutContextType) {
36 const layout = useMaybeLayoutContext();
37 layoutContext ??= layout;
38 if (!layoutContext) {
39 throw Error('Tried to access LayoutContext context outside a LayoutContextProvider provider.');
40 }
41 return layoutContext;
42}
43
44/** @public */
45export function useCreateLayoutContext(): LayoutContextType {
46 const [pinState, pinDispatch] = React.useReducer(pinReducer, PIN_DEFAULT_STATE);
47 const [widgetState, widgetDispatch] = React.useReducer(chatReducer, WIDGET_DEFAULT_STATE);
48 return {
49 pin: { dispatch: pinDispatch, state: pinState },
50 widget: { dispatch: widgetDispatch, state: widgetState },
51 };
52}
53
54/** @public */
55export function useEnsureCreateLayoutContext(layoutContext?: LayoutContextType): LayoutContextType {
56 const [pinState, pinDispatch] = React.useReducer(pinReducer, PIN_DEFAULT_STATE);
57 const [widgetState, widgetDispatch] = React.useReducer(chatReducer, WIDGET_DEFAULT_STATE);
58 return (
59 layoutContext ?? {
60 pin: { dispatch: pinDispatch, state: pinState },
61 widget: { dispatch: widgetDispatch, state: widgetState },
62 }
63 );
64}
65
66/**
67 * Returns a layout context from the `LayoutContext` if it exists, otherwise `undefined`.
68 * @public
69 */
70export function useMaybeLayoutContext(): LayoutContextType | undefined {
71 return React.useContext(LayoutContext);
72}
Added view/app/src/vendor/livekit/react/context/participant-context.ts view/app/src/vendor/livekit/react/context/participant-context.ts +44 −0 View view/app/src/vendor/livekit/react/context/participant-context.ts
@@ -0,0 +1,44 @@
1import type { Participant } from 'livekit-client';
2import * as React from 'react';
3import { useMaybeTrackRefContext } from './track-reference-context';
4
5/** @public */
6export const ParticipantContext = React.createContext<Participant | undefined>(undefined);
7
8/**
9 * Ensures that a participant is provided via context.
10 * If not inside a `ParticipantContext`, an error is thrown.
11 * @public
12 */
13export function useParticipantContext() {
14 const participant = React.useContext(ParticipantContext);
15 if (!participant) {
16 throw Error('tried to access participant context outside of participant context provider');
17 }
18 return participant;
19}
20
21/**
22 * Returns a participant from the `ParticipantContext` if it exists, otherwise `undefined`.
23 * @public
24 */
25export function useMaybeParticipantContext() {
26 return React.useContext(ParticipantContext);
27}
28
29/**
30 * Ensures that a participant is provided, either via context or explicitly as a parameter.
31 * If not inside a `ParticipantContext` and no participant is provided, an error is thrown.
32 * @public
33 */
34export function useEnsureParticipant(participant?: Participant) {
35 const context = useMaybeParticipantContext();
36 const trackContext = useMaybeTrackRefContext();
37 const p = participant ?? context ?? trackContext?.participant;
38 if (!p) {
39 throw new Error(
40 'No participant provided, make sure you are inside a participant context or pass the participant explicitly',
41 );
42 }
43 return p;
44}
Added view/app/src/vendor/livekit/react/context/pin-context.ts view/app/src/vendor/livekit/react/context/pin-context.ts +27 −0 View view/app/src/vendor/livekit/react/context/pin-context.ts
@@ -0,0 +1,27 @@
1import type { PinState, TrackReferenceOrPlaceholder } from '@livekit/components-core';
2import type * as React from 'react';
3
4/** @internal */
5export type PinAction =
6 | {
7 msg: 'set_pin';
8 trackReference: TrackReferenceOrPlaceholder;
9 }
10 | { msg: 'clear_pin' };
11
12/** @internal */
13export type PinContextType = {
14 dispatch?: React.Dispatch<PinAction>;
15 state?: PinState;
16};
17
18/** @internal */
19export function pinReducer(state: PinState, action: PinAction): PinState {
20 if (action.msg === 'set_pin') {
21 return [action.trackReference];
22 } else if (action.msg === 'clear_pin') {
23 return [];
24 } else {
25 return { ...state };
26 }
27}
Added view/app/src/vendor/livekit/react/context/room-context.ts view/app/src/vendor/livekit/react/context/room-context.ts +42 −0 View view/app/src/vendor/livekit/react/context/room-context.ts
@@ -0,0 +1,42 @@
1import type { Room } from 'livekit-client';
2import * as React from 'react';
3
4/** @public */
5export const RoomContext = React.createContext<Room | undefined>(undefined);
6
7/**
8 * Ensures that a room is provided via context.
9 * If no room is provided, an error is thrown.
10 * @public
11 */
12export function useRoomContext() {
13 const ctx = React.useContext(RoomContext);
14 if (!ctx) {
15 throw Error('tried to access room context outside of livekit room component');
16 }
17 return ctx;
18}
19
20/**
21 * Returns the room context if it exists, otherwise undefined.
22 * @public
23 */
24export function useMaybeRoomContext() {
25 return React.useContext(RoomContext);
26}
27
28/**
29 * Ensures that a room is provided, either via context or explicitly as a parameter.
30 * If no room is provided, an error is thrown.
31 * @public
32 */
33export function useEnsureRoom(room?: Room) {
34 const context = useMaybeRoomContext();
35 const r = room ?? context;
36 if (!r) {
37 throw new Error(
38 'No room provided, make sure you are inside a Room context or pass the room explicitly',
39 );
40 }
41 return r;
42}
Added view/app/src/vendor/livekit/react/context/track-reference-context.ts view/app/src/vendor/livekit/react/context/track-reference-context.ts +47 −0 View view/app/src/vendor/livekit/react/context/track-reference-context.ts
@@ -0,0 +1,47 @@
1import type { TrackReferenceOrPlaceholder } from '@livekit/components-core';
2import * as React from 'react';
3
4/**
5 * This context provides a `TrackReferenceOrPlaceholder` to all child components.
6 * @public
7 */
8export const TrackRefContext = React.createContext<TrackReferenceOrPlaceholder | undefined>(
9 undefined,
10);
11
12/**
13 * Ensures that a track reference is provided via context.
14 * If not inside a `TrackRefContext`, an error is thrown.
15 * @public
16 */
17export function useTrackRefContext() {
18 const trackReference = React.useContext(TrackRefContext);
19 if (!trackReference) {
20 throw Error('tried to access track context outside of track context provider');
21 }
22 return trackReference;
23}
24
25/**
26 * Returns a track reference from the `TrackRefContext` if it exists, otherwise `undefined`.
27 * @public
28 */
29export function useMaybeTrackRefContext() {
30 return React.useContext(TrackRefContext);
31}
32
33/**
34 * Ensures that a track reference is provided, either via context or explicitly as a parameter.
35 * If not inside a `TrackRefContext` and no track reference is provided, an error is thrown.
36 * @public
37 */
38export function useEnsureTrackRef(trackRef?: TrackReferenceOrPlaceholder) {
39 const context = useMaybeTrackRefContext();
40 const ref = trackRef ?? context;
41 if (!ref) {
42 throw new Error(
43 'No TrackRef, make sure you are inside a TrackRefContext or pass the TrackRef explicitly',
44 );
45 }
46 return ref;
47}
Added view/app/src/vendor/livekit/react/hooks/internal/index.ts view/app/src/vendor/livekit/react/hooks/internal/index.ts +10 −0 View view/app/src/vendor/livekit/react/hooks/internal/index.ts
@@ -0,0 +1,10 @@
1/**
2 * A collection of internal hooks that are not part of the public API
3 * and therefore can be modified or removed at any time.
4 *
5 * @internal
6 */
7
8export * from './useResizeObserver';
9export * from './useObservableState';
10export * from './useMediaQuery';
Added view/app/src/vendor/livekit/react/hooks/internal/useMediaQuery.ts view/app/src/vendor/livekit/react/hooks/internal/useMediaQuery.ts +46 −0 View view/app/src/vendor/livekit/react/hooks/internal/useMediaQuery.ts
@@ -0,0 +1,46 @@
1import * as React from 'react';
2/**
3 * Implementation used from https://github.com/juliencrn/usehooks-ts
4 *
5 * @internal
6 */
7export function useMediaQuery(query: string): boolean {
8 const getMatches = (query: string): boolean => {
9 // Prevents SSR issues
10 if (typeof window !== 'undefined') {
11 return window.matchMedia(query).matches;
12 }
13 return false;
14 };
15
16 const [matches, setMatches] = React.useState<boolean>(getMatches(query));
17
18 function handleChange() {
19 setMatches(getMatches(query));
20 }
21
22 React.useEffect(() => {
23 const matchMedia = window.matchMedia(query);
24
25 // Triggered at the first client-side load and if query changes
26 handleChange();
27
28 // Listen matchMedia
29 if (matchMedia.addListener) {
30 matchMedia.addListener(handleChange);
31 } else {
32 matchMedia.addEventListener('change', handleChange);
33 }
34
35 return () => {
36 if (matchMedia.removeListener) {
37 matchMedia.removeListener(handleChange);
38 } else {
39 matchMedia.removeEventListener('change', handleChange);
40 }
41 };
42 // eslint-disable-next-line react-hooks/exhaustive-deps
43 }, [query]);
44
45 return matches;
46}
Added view/app/src/vendor/livekit/react/hooks/internal/useObservableState.ts view/app/src/vendor/livekit/react/hooks/internal/useObservableState.ts +24 −0 View view/app/src/vendor/livekit/react/hooks/internal/useObservableState.ts
@@ -0,0 +1,24 @@
1import * as React from 'react';
2// @ts-ignore
3import type { Observable } from 'rxjs';
4
5/**
6 * @internal
7 */
8export function useObservableState<T>(
9 observable: Observable<T> | undefined,
10 startWith: T,
11 resetWhenObservableChanges = true,
12) {
13 const [state, setState] = React.useState<T>(startWith);
14 React.useEffect(() => {
15 if (resetWhenObservableChanges) {
16 setState(startWith);
17 }
18 // observable state doesn't run in SSR
19 if (typeof window === 'undefined' || !observable) return;
20 const subscription = observable.subscribe(setState);
21 return () => subscription.unsubscribe();
22 }, [observable, resetWhenObservableChanges]);
23 return state;
24}
Added view/app/src/vendor/livekit/react/hooks/internal/useResizeObserver.ts view/app/src/vendor/livekit/react/hooks/internal/useResizeObserver.ts +124 −0 View view/app/src/vendor/livekit/react/hooks/internal/useResizeObserver.ts
@@ -0,0 +1,124 @@
1/* eslint-disable no-return-assign */
2/* eslint-disable no-underscore-dangle */
3import * as React from 'react';
4
5const useLatest = <T>(current: T) => {
6 const storedValue = React.useRef(current);
7 React.useEffect(() => {
8 storedValue.current = current;
9 });
10 return storedValue;
11};
12
13/**
14 * A React hook that fires a callback whenever ResizeObserver detects a change to its size
15 * code extracted from https://github.com/jaredLunde/react-hook/blob/master/packages/resize-observer/src/index.tsx in order to not include the polyfill for resize-observer
16 *
17 * @internal
18 */
19export function useResizeObserver<T extends HTMLElement>(
20 target: React.RefObject<T | null>,
21 callback: UseResizeObserverCallback,
22) {
23 const resizeObserver = getResizeObserver();
24 const storedCallback = useLatest(callback);
25
26 React.useLayoutEffect(() => {
27 let didUnsubscribe = false;
28
29 const targetEl = target.current;
30 if (!targetEl) return;
31
32 function cb(entry: ResizeObserverEntry, observer: ResizeObserver) {
33 if (didUnsubscribe) return;
34 storedCallback.current(entry, observer);
35 }
36
37 resizeObserver?.subscribe(targetEl as HTMLElement, cb);
38
39 return () => {
40 didUnsubscribe = true;
41 resizeObserver?.unsubscribe(targetEl as HTMLElement, cb);
42 };
43 }, [target.current, resizeObserver, storedCallback]);
44
45 return resizeObserver?.observer;
46}
47
48function createResizeObserver() {
49 let ticking = false;
50 let allEntries: ResizeObserverEntry[] = [];
51
52 const callbacks: Map<unknown, Array<UseResizeObserverCallback>> = new Map();
53
54 if (typeof window === 'undefined') {
55 return;
56 }
57
58 const observer = new ResizeObserver((entries: ResizeObserverEntry[], obs: ResizeObserver) => {
59 allEntries = allEntries.concat(entries);
60 if (!ticking) {
61 window.requestAnimationFrame(() => {
62 const triggered = new Set<Element>();
63 for (let i = 0; i < allEntries.length; i++) {
64 if (triggered.has(allEntries[i].target)) continue;
65 triggered.add(allEntries[i].target);
66 const cbs = callbacks.get(allEntries[i].target);
67 cbs?.forEach((cb) => cb(allEntries[i], obs));
68 }
69 allEntries = [];
70 ticking = false;
71 });
72 }
73 ticking = true;
74 });
75
76 return {
77 observer,
78 subscribe(target: HTMLElement, callback: UseResizeObserverCallback) {
79 observer.observe(target);
80 const cbs = callbacks.get(target) ?? [];
81 cbs.push(callback);
82 callbacks.set(target, cbs);
83 },
84 unsubscribe(target: HTMLElement, callback: UseResizeObserverCallback) {
85 const cbs = callbacks.get(target) ?? [];
86 if (cbs.length === 1) {
87 observer.unobserve(target);
88 callbacks.delete(target);
89 return;
90 }
91 const cbIndex = cbs.indexOf(callback);
92 if (cbIndex !== -1) cbs.splice(cbIndex, 1);
93 callbacks.set(target, cbs);
94 },
95 };
96}
97
98let _resizeObserver: ReturnType<typeof createResizeObserver>;
99
100const getResizeObserver = () =>
101 !_resizeObserver ? (_resizeObserver = createResizeObserver()) : _resizeObserver;
102
103export type UseResizeObserverCallback = (
104 entry: ResizeObserverEntry,
105 observer: ResizeObserver,
106) => unknown;
107
108export const useSize = (target: React.RefObject<HTMLDivElement | null>) => {
109 const [size, setSize] = React.useState({ width: 0, height: 0 });
110 React.useLayoutEffect(() => {
111 if (target.current) {
112 const { width, height } = target.current.getBoundingClientRect();
113 setSize({ width, height });
114 }
115 }, [target.current]);
116
117 const resizeCallback = React.useCallback(
118 (entry: ResizeObserverEntry) => setSize(entry.contentRect),
119 [],
120 );
121 // Where the magic happens
122 useResizeObserver(target, resizeCallback);
123 return size;
124};
Added view/app/src/vendor/livekit/react/hooks/index.ts view/app/src/vendor/livekit/react/hooks/index.ts +58 −0 View view/app/src/vendor/livekit/react/hooks/index.ts
@@ -0,0 +1,58 @@
1export { useAudioPlayback } from './useAudioPlayback';
2export { useClearPinButton } from './useClearPinButton';
3export {
4 type ConnectionQualityIndicatorOptions,
5 useConnectionQualityIndicator,
6} from './useConnectionQualityIndicator';
7export { useConnectionState } from './useConnectionStatus';
8export { useDataChannel } from './useDataChannel';
9export { useDisconnectButton } from './useDisconnectButton';
10export { useFacingMode } from './useFacingMode';
11export { type UseFocusToggleProps, useFocusToggle } from './useFocusToggle';
12export { useGridLayout } from './useGridLayout';
13export { type UseIsMutedOptions, useIsMuted } from './useIsMuted';
14export { useIsSpeaking } from './useIsSpeaking';
15export { useLiveKitRoom } from './useLiveKitRoom';
16export { type UseLocalParticipantOptions, useLocalParticipant } from './useLocalParticipant';
17export { useLocalParticipantPermissions } from './useLocalParticipantPermissions';
18export { type UseMediaDeviceSelectProps, useMediaDeviceSelect } from './useMediaDeviceSelect';
19export { useMediaDevices } from './useMediaDevices';
20export { usePagination } from './usePagination';
21export { type UseParticipantInfoOptions, useParticipantInfo } from './useParticipantInfo';
22export {
23 type UseParticipantPermissionsOptions,
24 useParticipantPermissions,
25} from './useParticipantPermissions';
26export { type UseParticipantTileProps, useParticipantTile } from './useParticipantTile';
27export { type UseParticipantsOptions, useParticipants } from './useParticipants';
28export { usePinnedTracks } from './usePinnedTracks';
29export { type UseRemoteParticipantOptions, useRemoteParticipant } from './useRemoteParticipant';
30export { type UseRemoteParticipantsOptions, useRemoteParticipants } from './useRemoteParticipants';
31export { type UseRoomInfoOptions, useRoomInfo } from './useRoomInfo';
32export { useSortedParticipants } from './useSortedParticipants';
33export {
34 useSpeakingParticipants,
35 type UseSpeakingParticipantsOptions,
36} from './useSpeakingParticipants';
37export { type UseStartAudioProps, useStartAudio } from './useStartAudio';
38export { type UseStartVideoProps, useStartVideo } from './useStartVideo';
39export { type UseSwipeOptions, useSwipe } from './useSwipe';
40export { type UseChatToggleProps, useChatToggle } from './useChatToggle';
41export { type UseTokenOptions, type UserInfo, useToken } from './useToken';
42export { useTrackMutedIndicator } from './useTrackMutedIndicator';
43export { type UseTrackToggleProps, useTrackToggle } from './useTrackToggle';
44export { type UseTracksHookReturnType, type UseTracksOptions, useTracks } from './useTracks';
45export { type UseVisualStableUpdateOptions, useVisualStableUpdate } from './useVisualStableUpdate';
46// export { UseTrackOptions, useTrack } from './useTrack';
47export { useTrackByName } from './useTrackByName';
48export { useChat } from './useChat';
49export {
50 usePersistentUserChoices,
51 type UsePersistentUserChoicesOptions,
52} from './usePersistentUserChoices';
53export { type UseIsEncryptedOptions, useIsEncrypted } from './useIsEncrypted';
54export * from './useTrackVolume';
55export * from './useParticipantTracks';
56export * from './useParticipantAttributes';
57export * from './useIsRecording';
58export * from './useEvents';
Added view/app/src/vendor/livekit/react/hooks/useAudioPlayback.ts view/app/src/vendor/livekit/react/hooks/useAudioPlayback.ts +34 −0 View view/app/src/vendor/livekit/react/hooks/useAudioPlayback.ts
@@ -0,0 +1,34 @@
1import type { Room } from 'livekit-client';
2import * as React from 'react';
3import { useObservableState } from './internal';
4import { roomAudioPlaybackAllowedObservable } from '@livekit/components-core';
5import { useEnsureRoom } from '../context';
6
7/**
8 * In many browsers to start audio playback, the user must perform a user-initiated event such as clicking a button.
9 * The `useAudioPlayback` hook returns an object with a boolean `canPlayAudio` flag that indicates whether audio
10 * playback is allowed in the current context, as well as a `startAudio` function that can be called in a button
11 * `onClick` callback to start audio playback in the current context.
12 *
13 * @see Autoplay policy on MDN web docs for more info: {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy}
14 * @alpha
15 */
16export function useAudioPlayback(room?: Room): {
17 canPlayAudio: boolean;
18 startAudio: () => Promise<void>;
19} {
20 const roomEnsured = useEnsureRoom(room);
21 const startAudio = React.useCallback(async () => {
22 await roomEnsured.startAudio();
23 }, [roomEnsured]);
24
25 const observable = React.useMemo(
26 () => roomAudioPlaybackAllowedObservable(roomEnsured),
27 [roomEnsured],
28 );
29 const { canPlayAudio } = useObservableState(observable, {
30 canPlayAudio: roomEnsured.canPlaybackAudio,
31 });
32
33 return { canPlayAudio, startAudio };
34}
Added view/app/src/vendor/livekit/react/hooks/useChat.ts view/app/src/vendor/livekit/react/hooks/useChat.ts +57 −0 View view/app/src/vendor/livekit/react/hooks/useChat.ts
@@ -0,0 +1,57 @@
1import * as React from 'react';
2import type { ChatOptions, ReceivedChatMessage } from '@livekit/components-core';
3import { setupChat } from '@livekit/components-core';
4import { ConnectionState, Room } from 'livekit-client';
5import { useEnsureRoom } from '../context';
6import { useObservableState } from './internal/useObservableState';
7import { useConnectionState } from './useConnectionStatus';
8
9/**
10 * The `useChat` hook provides chat functionality for a LiveKit room.
11 *
12 * @remarks
13 * Message history is not persisted and will be lost if the component is refreshed.
14 * You may want to persist message history in the browser, a cache or a database.
15 *
16 * @returns An object containing:
17 * - `chatMessages` - Array of received chat messages
18 * - `send` - Function to send a new message
19 * - `isSending` - Boolean indicating if a message is currently being sent
20 *
21 * @example
22 * ```tsx
23 * function ChatComponent() {
24 * const { chatMessages, send, isSending } = useChat();
25 *
26 * return (
27 * <div>
28 * {chatMessages.map((msg) => (
29 * <div key={msg.timestamp}>
30 * {msg.from?.identity}: {msg.message}
31 * </div>
32 * ))}
33 * <button disabled={isSending} onClick={() => send("Hello!")}>
34 * Send Message
35 * </button>
36 * </div>
37 * );
38 * }
39 * ```
40 * @public
41 */
42export function useChat(options?: ChatOptions & { room?: Room }) {
43 const room = useEnsureRoom(options?.room);
44 const connectionState = useConnectionState(room);
45 const isDisconnected = React.useMemo(
46 () => connectionState === ConnectionState.Disconnected,
47 [connectionState],
48 ); // used to reset the messages on room disconnect
49 const setup = React.useMemo<ReturnType<typeof setupChat>>(
50 () => setupChat(room, options),
51 [room, options, isDisconnected],
52 );
53 const isSending = useObservableState(setup.isSendingObservable, false);
54 const chatMessages = useObservableState<ReceivedChatMessage[]>(setup.messageObservable, []);
55
56 return { send: setup.send, chatMessages, isSending };
57}
Added view/app/src/vendor/livekit/react/hooks/useChatToggle.ts view/app/src/vendor/livekit/react/hooks/useChatToggle.ts +38 −0 View view/app/src/vendor/livekit/react/hooks/useChatToggle.ts
@@ -0,0 +1,38 @@
1import { setupChatToggle } from '@livekit/components-core';
2import { useLayoutContext } from '../context';
3import { mergeProps } from '../mergeProps';
4import * as React from 'react';
5
6/** @public */
7export interface UseChatToggleProps {
8 props: React.ButtonHTMLAttributes<HTMLButtonElement>;
9}
10
11/**
12 * The `useChatToggle` hook provides state and functions for toggling the chat window.
13 * @remarks
14 * Depends on the `LayoutContext` to work properly.
15 * @see {@link ChatToggle}, {@link Chat}
16 * @public
17 */
18export function useChatToggle({ props }: UseChatToggleProps) {
19 const { dispatch, state } = useLayoutContext().widget;
20 const { className } = React.useMemo(() => setupChatToggle(), []);
21
22 const mergedProps = React.useMemo(() => {
23 return mergeProps(props, {
24 className,
25 onClick: () => {
26 if (dispatch) dispatch({ msg: 'toggle_chat' });
27 },
28 'aria-pressed': state?.showChat ? 'true' : 'false',
29 'data-lk-unread-msgs': state
30 ? state.unreadMessages < 10
31 ? state.unreadMessages.toFixed(0)
32 : '9+'
33 : '0',
34 });
35 }, [props, className, dispatch, state]);
36
37 return { mergedProps };
38}
Added view/app/src/vendor/livekit/react/hooks/useClearPinButton.ts view/app/src/vendor/livekit/react/hooks/useClearPinButton.ts +29 −0 View view/app/src/vendor/livekit/react/hooks/useClearPinButton.ts
@@ -0,0 +1,29 @@
1import { setupClearPinButton } from '@livekit/components-core';
2import * as React from 'react';
3import { useLayoutContext } from '../context';
4import { mergeProps } from '../mergeProps';
5import type { ClearPinButtonProps } from '../components';
6
7/**
8 * The `useClearPinButton` hook provides props for the {@link ClearPinButton}
9 * or your custom implementation of it component. It adds the `onClick` handler
10 * to signal the `LayoutContext` that the tile in focus should be cleared.
11 * @public
12 */
13export function useClearPinButton(props: ClearPinButtonProps) {
14 const { state, dispatch } = useLayoutContext().pin;
15
16 const buttonProps = React.useMemo(() => {
17 const { className } = setupClearPinButton();
18 const mergedProps = mergeProps(props, {
19 className,
20 disabled: !state?.length,
21 onClick: () => {
22 if (dispatch) dispatch({ msg: 'clear_pin' });
23 },
24 });
25 return mergedProps;
26 }, [props, dispatch, state]);
27
28 return { buttonProps };
29}
Added view/app/src/vendor/livekit/react/hooks/useConnectionQualityIndicator.ts view/app/src/vendor/livekit/react/hooks/useConnectionQualityIndicator.ts +33 −0 View view/app/src/vendor/livekit/react/hooks/useConnectionQualityIndicator.ts
@@ -0,0 +1,33 @@
1import { setupConnectionQualityIndicator } from '@livekit/components-core';
2import type { Participant } from 'livekit-client';
3import * as React from 'react';
4import { useEnsureParticipant } from '../context';
5import { useObservableState } from './internal';
6
7/** @public */
8export interface ConnectionQualityIndicatorOptions {
9 participant?: Participant;
10}
11
12/**
13 * The `useConnectionQualityIndicator` hook provides props for the `ConnectionQualityIndicator` or your custom implementation of it component.
14 * @example
15 * ```tsx
16 * const { quality } = useConnectionQualityIndicator();
17 * // or
18 * const { quality } = useConnectionQualityIndicator({ participant });
19 * ```
20 * @public
21 */
22export function useConnectionQualityIndicator(options: ConnectionQualityIndicatorOptions = {}) {
23 const p = useEnsureParticipant(options.participant);
24
25 const { className, connectionQualityObserver } = React.useMemo(
26 () => setupConnectionQualityIndicator(p),
27 [p],
28 );
29
30 const quality = useObservableState(connectionQualityObserver, p.connectionQuality);
31
32 return { className, quality };
33}
Added view/app/src/vendor/livekit/react/hooks/useConnectionStatus.ts view/app/src/vendor/livekit/react/hooks/useConnectionStatus.ts +22 −0 View view/app/src/vendor/livekit/react/hooks/useConnectionStatus.ts
@@ -0,0 +1,22 @@
1import { connectionStateObserver } from '@livekit/components-core';
2import type { Room } from 'livekit-client';
3import * as React from 'react';
4import { useEnsureRoom } from '../context';
5import { useObservableState } from './internal';
6
7/**
8 * The `useConnectionState` hook allows you to simply implement your own `ConnectionState` component.
9 *
10 * @example
11 * ```tsx
12 * const connectionState = useConnectionState(room);
13 * ```
14 * @public
15 */
16export function useConnectionState(room?: Room) {
17 // passed room takes precedence, if not supplied get current room context
18 const r = useEnsureRoom(room);
19 const observable = React.useMemo(() => connectionStateObserver(r), [r]);
20 const connectionState = useObservableState(observable, r.state);
21 return connectionState;
22}
Added view/app/src/vendor/livekit/react/hooks/useDataChannel.ts view/app/src/vendor/livekit/react/hooks/useDataChannel.ts +73 −0 View view/app/src/vendor/livekit/react/hooks/useDataChannel.ts
@@ -0,0 +1,73 @@
1import type { ReceivedDataMessage } from '@livekit/components-core';
2import { setupDataMessageHandler } from '@livekit/components-core';
3import * as React from 'react';
4import type { DataPublishOptions } from 'livekit-client';
5import { useRoomContext } from '../context';
6import { useObservableState } from './internal';
7
8type UseDataChannelReturnType<T extends string | undefined = undefined> = {
9 isSending: boolean;
10 send: (payload: Uint8Array, options: DataPublishOptions) => Promise<void>;
11 message: ReceivedDataMessage<T> | undefined;
12};
13
14/**
15 * The `useDataChannel` hook returns the ability to send and receive messages.
16 * Pass an optional `topic` to narrow down which messages are returned in the messages array.
17 *
18 * @remarks
19 * There is only one data channel. Passing a `topic` does not open a new data channel.
20 * It is only used to filter out messages with no or a different `topic`.
21 *
22 * @example
23 * ```tsx
24 * // Send messages to all participants via the 'chat' topic.
25 * const { message: latestMessage, send } = useDataChannel('chat', (msg) => console.log("message received", msg));
26 * ```
27 *
28 * @example
29 * ```tsx
30 * // Receive all messages (no topic filtering)
31 * const { message: latestMessage, send } = useDataChannel((msg) => console.log("message received", msg));
32 * ```
33 *
34 * @public
35 */
36export function useDataChannel<T extends string>(
37 topic: T,
38 onMessage?: (msg: ReceivedDataMessage<T>) => void,
39): UseDataChannelReturnType<T>;
40
41/**
42 * Overload for `useDataChannel` without a topic. See {@link (useDataChannel:1)} for information and usage examples.
43 *
44 * @public
45 */
46export function useDataChannel(
47 onMessage?: (msg: ReceivedDataMessage) => void,
48): UseDataChannelReturnType;
49/**
50 * @internal
51 */
52export function useDataChannel<T extends string>(
53 topicOrCallback?: T | ((msg: ReceivedDataMessage) => void),
54 callback?: (msg: ReceivedDataMessage<T>) => void,
55) {
56 const onMessage = typeof topicOrCallback === 'function' ? topicOrCallback : callback;
57
58 const topic = typeof topicOrCallback === 'string' ? topicOrCallback : undefined;
59 const room = useRoomContext();
60 const { send, messageObservable, isSendingObservable } = React.useMemo(
61 () => setupDataMessageHandler(room, topic, onMessage),
62 [room, topic, onMessage],
63 );
64
65 const message = useObservableState(messageObservable, undefined);
66 const isSending = useObservableState(isSendingObservable, false);
67
68 return {
69 message,
70 send,
71 isSending,
72 };
73}
Added view/app/src/vendor/livekit/react/hooks/useDisconnectButton.ts view/app/src/vendor/livekit/react/hooks/useDisconnectButton.ts +36 −0 View view/app/src/vendor/livekit/react/hooks/useDisconnectButton.ts
@@ -0,0 +1,36 @@
1import { setupDisconnectButton } from '@livekit/components-core';
2import { ConnectionState } from 'livekit-client';
3import * as React from 'react';
4import type { DisconnectButtonProps } from '../components';
5import { useRoomContext } from '../context';
6import { mergeProps } from '../mergeProps';
7import { useConnectionState } from './useConnectionStatus';
8
9/**
10 * The `useDisconnectButton` hook is used to implement the `DisconnectButton` or your
11 * custom implementation of it. It adds onClick handler to the button to disconnect
12 * from the room.
13 *
14 * @example
15 * ```tsx
16 * const { buttonProps } = useDisconnectButton(buttonProps);
17 * return <button {...buttonProps}>Disconnect</button>;
18 * ```
19 * @public
20 */
21export function useDisconnectButton(props: DisconnectButtonProps) {
22 const room = useRoomContext();
23 const connectionState = useConnectionState(room);
24
25 const buttonProps = React.useMemo(() => {
26 const { className, disconnect } = setupDisconnectButton(room);
27 const mergedProps = mergeProps(props, {
28 className,
29 onClick: () => disconnect(props.stopTracks ?? true),
30 disabled: connectionState === ConnectionState.Disconnected,
31 });
32 return mergedProps;
33 }, [room, props, connectionState]);
34
35 return { buttonProps };
36}
Added view/app/src/vendor/livekit/react/hooks/useEvents.ts view/app/src/vendor/livekit/react/hooks/useEvents.ts +40 −0 View view/app/src/vendor/livekit/react/hooks/useEvents.ts
@@ -0,0 +1,40 @@
1import * as React from 'react';
2import type TypedEventEmitter from 'typed-emitter';
3import type { EventMap } from 'typed-emitter';
4
5/** @public */
6export function useEvents<
7 Emitter extends TypedEventEmitter<EventMap>,
8 EmitterEventMap extends (Emitter extends TypedEventEmitter<infer EM> ? EM : never),
9 Event extends Parameters<Emitter['on']>[0],
10 Callback extends EmitterEventMap[Event],
11>(
12 instance: Emitter | { internal: { emitter: Emitter } } | null | undefined,
13 event: Event,
14 handlerFn: Callback | undefined,
15 dependencies?: React.DependencyList,
16) {
17 const fallback = React.useMemo(() => () => {}, []);
18 const wrappedCallback = React.useCallback(handlerFn ?? fallback, dependencies ?? []);
19 const callback = dependencies ? wrappedCallback : handlerFn;
20
21 const emitter = React.useMemo(() => {
22 if (!instance) {
23 return null;
24 }
25 if ('internal' in instance) {
26 return instance.internal.emitter;
27 }
28 return instance;
29 }, [instance]);
30
31 React.useEffect(() => {
32 if (!emitter || !callback) {
33 return;
34 }
35 emitter.on(event, callback);
36 return () => {
37 emitter.off(event, callback);
38 };
39 }, [emitter, event, callback]);
40}
Added view/app/src/vendor/livekit/react/hooks/useFacingMode.ts view/app/src/vendor/livekit/react/hooks/useFacingMode.ts +22 −0 View view/app/src/vendor/livekit/react/hooks/useFacingMode.ts
@@ -0,0 +1,22 @@
1import type { TrackReferenceOrPlaceholder } from '@livekit/components-core';
2import { LocalTrackPublication, facingModeFromLocalTrack } from 'livekit-client';
3
4/**
5 * Try to determine the `facingMode` of a local participant video track.
6 * @remarks
7 * Works only on local video tracks.
8 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/facingMode | MDN docs on facingMode}
9 * @alpha
10 */
11export function useFacingMode(
12 trackReference: TrackReferenceOrPlaceholder,
13): 'user' | 'environment' | 'left' | 'right' | 'undefined' {
14 if (trackReference.publication instanceof LocalTrackPublication) {
15 const localTrack = trackReference.publication.track;
16 if (localTrack) {
17 const { facingMode } = facingModeFromLocalTrack(localTrack);
18 return facingMode;
19 }
20 }
21 return 'undefined';
22}
Added view/app/src/vendor/livekit/react/hooks/useFocusToggle.ts view/app/src/vendor/livekit/react/hooks/useFocusToggle.ts +59 −0 View view/app/src/vendor/livekit/react/hooks/useFocusToggle.ts
@@ -0,0 +1,59 @@
1import type { TrackReferenceOrPlaceholder } from '@livekit/components-core';
2import { setupFocusToggle, isTrackReferencePinned } from '@livekit/components-core';
3import { useEnsureTrackRef, useMaybeLayoutContext } from '../context';
4import { mergeProps } from '../mergeProps';
5import * as React from 'react';
6
7/** @public */
8export interface UseFocusToggleProps {
9 trackRef?: TrackReferenceOrPlaceholder;
10 props: React.ButtonHTMLAttributes<HTMLButtonElement>;
11}
12
13/**
14 * The `useFocusToggle` hook is used to implement the `FocusToggle` or your custom implementation of it.
15 * The `TrackReferenceOrPlaceholder` is used to register a onClick handler and to identify the track to focus on.
16 *
17 * @example
18 * ```tsx
19 * const { mergedProps, inFocus } = useFocusToggle({ trackRef, props: yourButtonProps });
20 * return <button {...mergedProps}>{inFocus ? 'Unfocus' : 'Focus'}</button>;
21 * ```
22 * @public
23 */
24export function useFocusToggle({ trackRef, props }: UseFocusToggleProps) {
25 const trackReference = useEnsureTrackRef(trackRef);
26
27 const layoutContext = useMaybeLayoutContext();
28 const { className } = React.useMemo(() => setupFocusToggle(), []);
29
30 const inFocus: boolean = React.useMemo(() => {
31 return isTrackReferencePinned(trackReference, layoutContext?.pin.state);
32 }, [trackReference, layoutContext?.pin.state]);
33
34 const mergedProps = React.useMemo(
35 () =>
36 mergeProps(props, {
37 className,
38 onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
39 // Call user defined on click callbacks.
40 props.onClick?.(event);
41
42 // Set or clear focus based on current focus state.
43 if (inFocus) {
44 layoutContext?.pin.dispatch?.({
45 msg: 'clear_pin',
46 });
47 } else {
48 layoutContext?.pin.dispatch?.({
49 msg: 'set_pin',
50 trackReference,
51 });
52 }
53 },
54 }),
55 [props, className, trackReference, inFocus, layoutContext?.pin],
56 );
57
58 return { mergedProps, inFocus };
59}
Added view/app/src/vendor/livekit/react/hooks/useGridLayout.ts view/app/src/vendor/livekit/react/hooks/useGridLayout.ts +44 −0 View view/app/src/vendor/livekit/react/hooks/useGridLayout.ts
@@ -0,0 +1,44 @@
1import { GRID_LAYOUTS, selectGridLayout } from '@livekit/components-core';
2import type { GridLayoutDefinition, GridLayoutInfo } from '@livekit/components-core';
3import * as React from 'react';
4import { useSize } from './internal';
5
6/**
7 * The `useGridLayout` hook tries to select the best layout to fit all tiles.
8 * If the available screen space is not enough, it will reduce the number of maximum visible
9 * tiles and select a layout that still works visually within the given limitations.
10 * As the order of tiles changes over time, the hook tries to keep visual updates to a minimum
11 * while trying to display important tiles such as speaking participants or screen shares.
12 *
13 * @example
14 * ```tsx
15 * const { layout } = useGridLayout(gridElement, trackCount);
16 * ```
17 * @public
18 */
19export function useGridLayout(
20 /** HTML element that contains the grid. */
21 gridElement: React.RefObject<HTMLDivElement | null>,
22 /** Count of tracks that should get layed out */
23 trackCount: number,
24 options: {
25 gridLayouts?: GridLayoutDefinition[];
26 } = {},
27): { layout: GridLayoutInfo; containerWidth: number; containerHeight: number } {
28 const gridLayouts = options.gridLayouts ?? GRID_LAYOUTS;
29 const { width, height } = useSize(gridElement);
30 const layout = selectGridLayout(gridLayouts, trackCount, width, height);
31
32 React.useEffect(() => {
33 if (gridElement.current && layout) {
34 gridElement.current.style.setProperty('--lk-col-count', layout?.columns.toString());
35 gridElement.current.style.setProperty('--lk-row-count', layout?.rows.toString());
36 }
37 }, [gridElement, layout]);
38
39 return {
40 layout,
41 containerWidth: width,
42 containerHeight: height,
43 };
44}
Added view/app/src/vendor/livekit/react/hooks/useIsEncrypted.ts view/app/src/vendor/livekit/react/hooks/useIsEncrypted.ts +29 −0 View view/app/src/vendor/livekit/react/hooks/useIsEncrypted.ts
@@ -0,0 +1,29 @@
1import * as React from 'react';
2import { LocalParticipant } from 'livekit-client';
3import type { Participant, Room } from 'livekit-client';
4import { encryptionStatusObservable } from '@livekit/components-core';
5import { useEnsureParticipant, useEnsureRoom } from '../context';
6import { useObservableState } from './internal';
7
8/**
9 * @alpha
10 */
11export interface UseIsEncryptedOptions {
12 room?: Room;
13}
14
15/**
16 * @alpha
17 */
18export function useIsEncrypted(participant?: Participant, options: UseIsEncryptedOptions = {}) {
19 const p = useEnsureParticipant(participant);
20
21 const room = useEnsureRoom(options.room);
22
23 const observer = React.useMemo(() => encryptionStatusObservable(room, p), [room, p]);
24 const isEncrypted = useObservableState(
25 observer,
26 p.isLocal ? (p as LocalParticipant).isE2EEEnabled : !!p?.isEncrypted,
27 );
28 return isEncrypted;
29}
Added view/app/src/vendor/livekit/react/hooks/useIsMuted.ts view/app/src/vendor/livekit/react/hooks/useIsMuted.ts +51 −0 View view/app/src/vendor/livekit/react/hooks/useIsMuted.ts
@@ -0,0 +1,51 @@
1import {
2 type TrackReferenceOrPlaceholder,
3 getTrackReferenceId,
4 mutedObserver,
5} from '@livekit/components-core';
6import type { Participant, Track } from 'livekit-client';
7import * as React from 'react';
8import { useEnsureParticipant } from '../context';
9
10/** @public */
11export interface UseIsMutedOptions {
12 participant?: Participant;
13}
14
15/**
16 * The `useIsMuted` hook is used to implement the `TrackMutedIndicator` or your custom implementation of it.
17 * It returns a `boolean` that indicates if the track is muted or not.
18 *
19 * @example With a track reference
20 * ```tsx
21 * const isMuted = useIsMuted(track);
22 * ```
23 *
24 * @param trackRef - A `TrackReference` indicating the track to monitor.
25 * @returns boolean indicating if the track is muted
26 *
27 * @public
28 */
29export function useIsMuted(trackRef: TrackReferenceOrPlaceholder): boolean;
30export function useIsMuted(
31 sourceOrTrackRef: TrackReferenceOrPlaceholder | Track.Source,
32 options: UseIsMutedOptions = {},
33) {
34 const passedParticipant =
35 typeof sourceOrTrackRef === 'string' ? options.participant : sourceOrTrackRef.participant;
36 const p = useEnsureParticipant(passedParticipant);
37 const ref =
38 typeof sourceOrTrackRef === 'string'
39 ? { participant: p, source: sourceOrTrackRef }
40 : sourceOrTrackRef;
41 const [isMuted, setIsMuted] = React.useState(
42 !!(ref.publication?.isMuted || p.getTrackPublication(ref.source)?.isMuted),
43 );
44
45 React.useEffect(() => {
46 const listener = mutedObserver(ref).subscribe(setIsMuted);
47 return () => listener.unsubscribe();
48 }, [getTrackReferenceId(ref)]);
49
50 return isMuted;
51}
Added view/app/src/vendor/livekit/react/hooks/useIsRecording.ts view/app/src/vendor/livekit/react/hooks/useIsRecording.ts +23 −0 View view/app/src/vendor/livekit/react/hooks/useIsRecording.ts
@@ -0,0 +1,23 @@
1import { recordingStatusObservable } from '@livekit/components-core';
2import * as React from 'react';
3import { useEnsureRoom } from '../context';
4import { useObservableState } from './internal';
5import { useConnectionState } from './useConnectionStatus';
6import type { Room } from 'livekit-client';
7
8/**
9 * The `useIsRecording` hook returns a `boolean` that indicates if the room is currently being recorded.
10 * @example
11 * ```tsx
12 * const isRecording = useIsRecording();
13 * ```
14 * @public
15 */
16export function useIsRecording(room?: Room) {
17 const r = useEnsureRoom(room);
18 const connectionState = useConnectionState(r);
19 const observable = React.useMemo(() => recordingStatusObservable(r), [r, connectionState]);
20 const isRecording = useObservableState(observable, r.isRecording);
21
22 return isRecording;
23}
Added view/app/src/vendor/livekit/react/hooks/useIsSpeaking.ts view/app/src/vendor/livekit/react/hooks/useIsSpeaking.ts +21 −0 View view/app/src/vendor/livekit/react/hooks/useIsSpeaking.ts
@@ -0,0 +1,21 @@
1import { createIsSpeakingObserver } from '@livekit/components-core';
2import type { Participant } from 'livekit-client';
3import * as React from 'react';
4import { useEnsureParticipant } from '../context';
5import { useObservableState } from './internal';
6
7/**
8 * The `useIsSpeaking` hook returns a `boolean` that indicates if the participant is speaking or not.
9 * @example
10 * ```tsx
11 * const isSpeaking = useIsSpeaking(participant);
12 * ```
13 * @public
14 */
15export function useIsSpeaking(participant?: Participant) {
16 const p = useEnsureParticipant(participant);
17 const observable = React.useMemo(() => createIsSpeakingObserver(p), [p]);
18 const isSpeaking = useObservableState(observable, p.isSpeaking);
19
20 return isSpeaking;
21}
Added view/app/src/vendor/livekit/react/hooks/useLiveKitRoom.ts view/app/src/vendor/livekit/react/hooks/useLiveKitRoom.ts +186 −0 View view/app/src/vendor/livekit/react/hooks/useLiveKitRoom.ts
@@ -0,0 +1,186 @@
1import { log, setupLiveKitRoom } from '@livekit/components-core';
2import type { DisconnectReason } from 'livekit-client';
3import { Room, MediaDeviceFailure, RoomEvent } from 'livekit-client';
4import * as React from 'react';
5import type { HTMLAttributes } from 'react';
6
7import type { LiveKitRoomProps } from '../components';
8import { mergeProps } from '../mergeProps';
9import { roomOptionsStringifyReplacer } from '../utils';
10
11const defaultRoomProps: Partial<LiveKitRoomProps> = {
12 connect: true,
13 audio: false,
14 video: false,
15};
16
17/**
18 * The `useLiveKitRoom` hook is used to implement the `LiveKitRoom` or your custom implementation of it.
19 * It returns a `Room` instance and HTML props that should be applied to the root element of the component.
20 *
21 * @example
22 * ```tsx
23 * const { room, htmlProps } = useLiveKitRoom();
24 * return <div {...htmlProps}>...</div>;
25 * ```
26 * @public
27 */
28export function useLiveKitRoom<T extends HTMLElement>(
29 props: LiveKitRoomProps,
30): {
31 room: Room | undefined;
32 htmlProps: HTMLAttributes<T>;
33} {
34 const {
35 token,
36 serverUrl,
37 options,
38 room: passedRoom,
39 connectOptions,
40 connect,
41 audio,
42 video,
43 screen,
44 onConnected,
45 onDisconnected,
46 onError,
47 onMediaDeviceFailure,
48 onEncryptionError,
49 simulateParticipants,
50 ...rest
51 } = { ...defaultRoomProps, ...props };
52 if (options && passedRoom) {
53 log.warn(
54 'when using a manually created room, the options object will be ignored. set the desired options directly when creating the room instead.',
55 );
56 }
57
58 const [room, setRoom] = React.useState<Room | undefined>();
59
60 const shouldConnect = React.useRef(connect);
61
62 React.useEffect(() => {
63 setRoom(passedRoom ?? new Room(options));
64 }, [passedRoom, JSON.stringify(options, roomOptionsStringifyReplacer)]);
65
66 const htmlProps = React.useMemo(() => {
67 const { className } = setupLiveKitRoom();
68 return mergeProps(rest, { className }) as HTMLAttributes<T>;
69 }, [rest]);
70
71 React.useEffect(() => {
72 if (!room) return;
73 const onSignalConnected = () => {
74 const localP = room.localParticipant;
75
76 log.debug('trying to publish local tracks');
77 Promise.all([
78 localP.setMicrophoneEnabled(!!audio, typeof audio !== 'boolean' ? audio : undefined),
79 localP.setCameraEnabled(!!video, typeof video !== 'boolean' ? video : undefined),
80 localP.setScreenShareEnabled(!!screen, typeof screen !== 'boolean' ? screen : undefined),
81 ]).catch((e) => {
82 log.warn(e);
83 onError?.(e as Error);
84 });
85 };
86
87 const handleMediaDeviceError = (e: Error, kind?: MediaDeviceKind) => {
88 const mediaDeviceFailure = MediaDeviceFailure.getFailure(e);
89 onMediaDeviceFailure?.(mediaDeviceFailure, kind);
90 };
91 const handleEncryptionError = (e: Error) => {
92 onEncryptionError?.(e);
93 };
94 const handleDisconnected = (reason?: DisconnectReason) => {
95 onDisconnected?.(reason);
96 };
97 const handleConnected = () => {
98 onConnected?.();
99 };
100
101 room
102 .on(RoomEvent.SignalConnected, onSignalConnected)
103 .on(RoomEvent.MediaDevicesError, handleMediaDeviceError)
104 .on(RoomEvent.EncryptionError, handleEncryptionError)
105 .on(RoomEvent.Disconnected, handleDisconnected)
106 .on(RoomEvent.Connected, handleConnected);
107
108 return () => {
109 room
110 .off(RoomEvent.SignalConnected, onSignalConnected)
111 .off(RoomEvent.MediaDevicesError, handleMediaDeviceError)
112 .off(RoomEvent.EncryptionError, handleEncryptionError)
113 .off(RoomEvent.Disconnected, handleDisconnected)
114 .off(RoomEvent.Connected, handleConnected);
115 };
116 }, [
117 room,
118 audio,
119 video,
120 screen,
121 onError,
122 onEncryptionError,
123 onMediaDeviceFailure,
124 onConnected,
125 onDisconnected,
126 ]);
127
128 React.useEffect(() => {
129 if (!room) return;
130
131 if (simulateParticipants) {
132 room.simulateParticipants({
133 participants: {
134 count: simulateParticipants,
135 },
136 publish: {
137 audio: true,
138 useRealTracks: true,
139 },
140 });
141 return;
142 }
143
144 if (connect) {
145 shouldConnect.current = true;
146 log.debug('connecting');
147 if (!token) {
148 log.debug('no token yet');
149 return;
150 }
151 if (!serverUrl) {
152 log.warn('no livekit url provided');
153 onError?.(Error('no livekit url provided'));
154 return;
155 }
156 room.connect(serverUrl, token, connectOptions).catch((e) => {
157 log.warn(e);
158 if (shouldConnect.current === true) {
159 onError?.(e as Error);
160 }
161 });
162 } else {
163 log.debug('disconnecting because connect is false');
164 shouldConnect.current = false;
165 room.disconnect();
166 }
167 }, [
168 connect,
169 token,
170 JSON.stringify(connectOptions),
171 room,
172 onError,
173 serverUrl,
174 simulateParticipants,
175 ]);
176
177 React.useEffect(() => {
178 if (!room) return;
179 return () => {
180 log.info('disconnecting on onmount');
181 room.disconnect();
182 };
183 }, [room]);
184
185 return { room, htmlProps };
186}
Added view/app/src/vendor/livekit/react/hooks/useLocalParticipant.ts view/app/src/vendor/livekit/react/hooks/useLocalParticipant.ts +73 −0 View view/app/src/vendor/livekit/react/hooks/useLocalParticipant.ts
@@ -0,0 +1,73 @@
1import type { ParticipantMedia } from '@livekit/components-core';
2import { observeParticipantMedia } from '@livekit/components-core';
3import type { TrackPublication, LocalParticipant, Room } from 'livekit-client';
4import * as React from 'react';
5import { useEnsureRoom } from '../context';
6
7/** @public */
8export interface UseLocalParticipantOptions {
9 /**
10 * The room to use. If not provided, the hook will use the room from the context.
11 */
12 room?: Room;
13}
14
15/**
16 * The `useLocalParticipant` hook returns the local participant and the associated state
17 * around the participant.
18 *
19 * @example
20 * ```tsx
21 * const { localParticipant } = useLocalParticipant();
22 * ```
23 * @public
24 */
25export function useLocalParticipant(options: UseLocalParticipantOptions = {}) {
26 const room = useEnsureRoom(options.room);
27 const [localParticipant, setLocalParticipant] = React.useState(room.localParticipant);
28
29 const [isMicrophoneEnabled, setIsMicrophoneEnabled] = React.useState(
30 localParticipant.isMicrophoneEnabled,
31 );
32 const [isCameraEnabled, setIsCameraEnabled] = React.useState(localParticipant.isCameraEnabled);
33 const [isScreenShareEnabled, setIsScreenShareEnabled] = React.useState(
34 localParticipant.isScreenShareEnabled,
35 );
36
37 const [lastMicrophoneError, setLastMicrophoneError] = React.useState(
38 localParticipant.lastMicrophoneError,
39 );
40 const [lastCameraError, setLastCameraError] = React.useState(localParticipant.lastCameraError);
41
42 const [microphoneTrack, setMicrophoneTrack] = React.useState<TrackPublication | undefined>(
43 undefined,
44 );
45 const [cameraTrack, setCameraTrack] = React.useState<TrackPublication | undefined>(undefined);
46
47 const handleUpdate = (media: ParticipantMedia<LocalParticipant>) => {
48 setIsCameraEnabled(media.isCameraEnabled);
49 setIsMicrophoneEnabled(media.isMicrophoneEnabled);
50 setIsScreenShareEnabled(media.isScreenShareEnabled);
51 setCameraTrack(media.cameraTrack);
52 setMicrophoneTrack(media.microphoneTrack);
53 setLastMicrophoneError(media.participant.lastMicrophoneError);
54 setLastCameraError(media.participant.lastCameraError);
55 setLocalParticipant(media.participant);
56 };
57 React.useEffect(() => {
58 const listener = observeParticipantMedia(room.localParticipant).subscribe(handleUpdate);
59 // TODO also listen to permission and metadata etc. events
60 return () => listener.unsubscribe();
61 }, [room]);
62
63 return {
64 isMicrophoneEnabled,
65 isScreenShareEnabled,
66 isCameraEnabled,
67 microphoneTrack,
68 cameraTrack,
69 lastMicrophoneError,
70 lastCameraError,
71 localParticipant,
72 };
73}
Added view/app/src/vendor/livekit/react/hooks/useLocalParticipantPermissions.ts view/app/src/vendor/livekit/react/hooks/useLocalParticipantPermissions.ts +24 −0 View view/app/src/vendor/livekit/react/hooks/useLocalParticipantPermissions.ts
@@ -0,0 +1,24 @@
1import { participantPermissionObserver } from '@livekit/components-core';
2import type { ParticipantPermission } from '@livekit/protocol';
3import * as React from 'react';
4import { useRoomContext } from '../context';
5import { useObservableState } from './internal';
6
7/**
8 * The `useLocalParticipantPermissions` hook returns the local participant's permissions.
9 *
10 * @example
11 * ```tsx
12 * const { canPublish, canPublishData } = useLocalParticipantPermissions();
13 * ```
14 * @public
15 */
16export function useLocalParticipantPermissions(): ParticipantPermission | undefined {
17 const room = useRoomContext();
18 const permissionObserver = React.useMemo(
19 () => participantPermissionObserver(room.localParticipant),
20 [room],
21 );
22 const permissions = useObservableState(permissionObserver, room.localParticipant.permissions);
23 return permissions;
24}
Added view/app/src/vendor/livekit/react/hooks/useMediaDevices.ts view/app/src/vendor/livekit/react/hooks/useMediaDevices.ts +28 −0 View view/app/src/vendor/livekit/react/hooks/useMediaDevices.ts
@@ -0,0 +1,28 @@
1import * as React from 'react';
2import { useObservableState } from './internal';
3import { createMediaDeviceObserver } from '@livekit/components-core';
4
5/**
6 * The `useMediaDevices` hook returns the list of media devices of a given kind.
7 *
8 * @example
9 * ```tsx
10 * const videoDevices = useMediaDevices({ kind: 'videoinput' });
11 * const audioDevices = useMediaDevices({ kind: 'audioinput' });
12 * ```
13 * @public
14 */
15export function useMediaDevices({
16 kind,
17 onError,
18}: {
19 kind: MediaDeviceKind;
20 onError?: (e: Error) => void;
21}) {
22 const deviceObserver = React.useMemo(
23 () => createMediaDeviceObserver(kind, onError),
24 [kind, onError],
25 );
26 const devices = useObservableState(deviceObserver, [] as MediaDeviceInfo[]);
27 return devices;
28}
Added view/app/src/vendor/livekit/react/hooks/useMediaDeviceSelect.ts view/app/src/vendor/livekit/react/hooks/useMediaDeviceSelect.ts +81 −0 View view/app/src/vendor/livekit/react/hooks/useMediaDeviceSelect.ts
@@ -0,0 +1,81 @@
1import { createMediaDeviceObserver, setupDeviceSelector, log } from '@livekit/components-core';
2import { Room, type LocalAudioTrack, type LocalVideoTrack } from 'livekit-client';
3import * as React from 'react';
4import { useMaybeRoomContext } from '../context';
5import { useObservableState } from './internal';
6
7/** @public */
8export interface UseMediaDeviceSelectProps {
9 kind: MediaDeviceKind;
10 room?: Room;
11 track?: LocalAudioTrack | LocalVideoTrack;
12 /**
13 * this will call getUserMedia if the permissions are not yet given to enumerate the devices with device labels.
14 * in some browsers multiple calls to getUserMedia result in multiple permission prompts.
15 * It's generally advised only flip this to true, once a (preview) track has been acquired successfully with the
16 * appropriate permissions.
17 *
18 * @see {@link MediaDeviceMenu}
19 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices | MDN enumerateDevices}
20 */
21 requestPermissions?: boolean;
22 /**
23 * this callback gets called if an error is thrown when failing to select a device and also if a user
24 * denied permissions, eventhough the `requestPermissions` option is set to `true`.
25 * Most commonly this will emit a MediaDeviceError
26 */
27 onError?: (e: Error) => void;
28}
29
30/**
31 * The `useMediaDeviceSelect` hook is used to implement the `MediaDeviceSelect` component and
32 * returns o.a. the list of devices of a given kind (audioinput or videoinput), the currently active device
33 * and a function to set the the active device.
34 *
35 * @example
36 * ```tsx
37 * const { devices, activeDeviceId, setActiveMediaDevice } = useMediaDeviceSelect({kind: 'audioinput'});
38 * ```
39 * @public
40 */
41export function useMediaDeviceSelect({
42 kind,
43 room,
44 track,
45 requestPermissions,
46 onError,
47}: UseMediaDeviceSelectProps) {
48 const roomContext = useMaybeRoomContext();
49
50 const roomFallback = React.useMemo(() => room ?? roomContext ?? new Room(), [room, roomContext]);
51
52 // List of all devices.
53 const deviceObserver = React.useMemo(
54 () => createMediaDeviceObserver(kind, onError, requestPermissions),
55 [kind, requestPermissions, onError],
56 );
57 const devices = useObservableState(deviceObserver, [] as MediaDeviceInfo[]);
58 // Active device management.
59 const [currentDeviceId, setCurrentDeviceId] = React.useState<string>(
60 roomFallback?.getActiveDevice(kind) ?? 'default',
61 );
62 const { className, activeDeviceObservable, setActiveMediaDevice } = React.useMemo(
63 () => setupDeviceSelector(kind, roomFallback, track),
64 [kind, roomFallback, track],
65 );
66
67 React.useEffect(() => {
68 const listener = activeDeviceObservable.subscribe((deviceId) => {
69 if (!deviceId) {
70 return;
71 }
72 log.info('setCurrentDeviceId', deviceId);
73 setCurrentDeviceId(deviceId);
74 });
75 return () => {
76 listener?.unsubscribe();
77 };
78 }, [activeDeviceObservable]);
79
80 return { devices, className, activeDeviceId: currentDeviceId, setActiveMediaDevice };
81}
Added view/app/src/vendor/livekit/react/hooks/useMediaTrackBySourceOrName.ts view/app/src/vendor/livekit/react/hooks/useMediaTrackBySourceOrName.ts +97 −0 View view/app/src/vendor/livekit/react/hooks/useMediaTrackBySourceOrName.ts
@@ -0,0 +1,97 @@
1import type { TrackIdentifier } from '@livekit/components-core';
2import {
3 getTrackByIdentifier,
4 isTrackReference,
5 log,
6 setupMediaTrack,
7} from '@livekit/components-core';
8import * as React from 'react';
9import { mergeProps } from '../utils';
10
11/** @public */
12export interface UseMediaTrackOptions {
13 element?: React.RefObject<HTMLMediaElement | null> | null;
14 props?: React.HTMLAttributes<HTMLVideoElement | HTMLAudioElement>;
15}
16
17/**
18 * @internal
19 */
20export function useMediaTrackBySourceOrName(
21 observerOptions: TrackIdentifier,
22 options: UseMediaTrackOptions = {},
23) {
24 const [publication, setPublication] = React.useState(getTrackByIdentifier(observerOptions));
25
26 const [isMuted, setMuted] = React.useState(publication?.isMuted);
27 const [isSubscribed, setSubscribed] = React.useState(publication?.isSubscribed);
28
29 const [track, setTrack] = React.useState(publication?.track);
30 const [orientation, setOrientation] = React.useState<'landscape' | 'portrait'>('landscape');
31 const previousElement = React.useRef<HTMLMediaElement | undefined | null>(null);
32
33 const { className, trackObserver } = React.useMemo(() => {
34 return setupMediaTrack(observerOptions);
35 }, [
36 observerOptions.participant.sid ?? observerOptions.participant.identity,
37 observerOptions.source,
38 isTrackReference(observerOptions) && observerOptions.publication.trackSid,
39 ]);
40
41 React.useEffect(() => {
42 const subscription = trackObserver.subscribe((publication) => {
43 log.debug('update track', publication);
44 setPublication(publication);
45 setMuted(publication?.isMuted);
46 setSubscribed(publication?.isSubscribed);
47 setTrack(publication?.track);
48 });
49 return () => subscription?.unsubscribe();
50 }, [trackObserver]);
51
52 React.useEffect(() => {
53 if (track) {
54 if (previousElement.current) {
55 track.detach(previousElement.current);
56 }
57 if (
58 options.element?.current &&
59 !(observerOptions.participant.isLocal && track?.kind === 'audio')
60 ) {
61 track.attach(options.element.current);
62 }
63 }
64 previousElement.current = options.element?.current;
65 return () => {
66 if (previousElement.current) {
67 track?.detach(previousElement.current);
68 }
69 };
70 }, [track, options.element]);
71
72 React.useEffect(() => {
73 // Set the orientation of the video track.
74 // TODO: This does not handle changes in orientation after a track got published (e.g when rotating a phone camera from portrait to landscape).
75 if (
76 typeof publication?.dimensions?.width === 'number' &&
77 typeof publication?.dimensions?.height === 'number'
78 ) {
79 const orientation_ =
80 publication.dimensions.width > publication.dimensions.height ? 'landscape' : 'portrait';
81 setOrientation(orientation_);
82 }
83 }, [publication]);
84
85 return {
86 publication,
87 isMuted,
88 isSubscribed,
89 track,
90 elementProps: mergeProps(options.props, {
91 className,
92 'data-lk-local-participant': observerOptions.participant.isLocal,
93 'data-lk-source': publication?.source,
94 ...(publication?.kind === 'video' && { 'data-lk-orientation': orientation }),
95 }),
96 };
97}
Added view/app/src/vendor/livekit/react/hooks/usePagination.ts view/app/src/vendor/livekit/react/hooks/usePagination.ts +67 −0 View view/app/src/vendor/livekit/react/hooks/usePagination.ts
@@ -0,0 +1,67 @@
1import type { TrackReferenceOrPlaceholder } from '@livekit/components-core';
2import * as React from 'react';
3import { useVisualStableUpdate } from './useVisualStableUpdate';
4
5/**
6 * The `usePagination` hook implements simple pagination logic for use with arrays.
7 * @example
8 * ```tsx
9 * const tracks = useTracks();
10 * const pagination = usePagination(4, tracks);
11 *
12 * <TrackLoop tracks={pagination.tracks} />
13 * ```
14 * @alpha
15 */
16export function usePagination(itemPerPage: number, trackReferences: TrackReferenceOrPlaceholder[]) {
17 const [currentPage, setCurrentPage] = React.useState(1);
18 const totalPageCount = Math.max(Math.ceil(trackReferences.length / itemPerPage), 1);
19 if (currentPage > totalPageCount) {
20 setCurrentPage(totalPageCount);
21 }
22 const lastItemIndex = currentPage * itemPerPage;
23 const firstItemIndex = lastItemIndex - itemPerPage;
24
25 const changePage = (direction: 'next' | 'previous') => {
26 setCurrentPage((state) => {
27 if (direction === 'next') {
28 if (state === totalPageCount) {
29 return state;
30 }
31 return state + 1;
32 } else {
33 if (state === 1) {
34 return state;
35 }
36 return state - 1;
37 }
38 });
39 };
40
41 const goToPage = (num: number) => {
42 if (num > totalPageCount) {
43 setCurrentPage(totalPageCount);
44 } else if (num < 1) {
45 setCurrentPage(1);
46 } else {
47 setCurrentPage(num);
48 }
49 };
50
51 const updatedTrackReferences = useVisualStableUpdate(trackReferences, itemPerPage);
52
53 const tracksOnPage = updatedTrackReferences.slice(firstItemIndex, lastItemIndex);
54
55 return {
56 totalPageCount,
57 nextPage: () => changePage('next'),
58 prevPage: () => changePage('previous'),
59 setPage: goToPage,
60 firstItemIndex,
61 lastItemIndex,
62 tracks: tracksOnPage,
63 currentPage,
64 };
65}
66
67export default usePagination;
Added view/app/src/vendor/livekit/react/hooks/useParticipantAttributes.ts view/app/src/vendor/livekit/react/hooks/useParticipantAttributes.ts +70 −0 View view/app/src/vendor/livekit/react/hooks/useParticipantAttributes.ts
@@ -0,0 +1,70 @@
1import { participantAttributesObserver } from '@livekit/components-core';
2import type { Participant } from 'livekit-client';
3import * as React from 'react';
4import { useEnsureParticipant, useMaybeParticipantContext } from '../context';
5import { useObservableState } from './internal';
6
7/**
8 * The `useParticipantAttributes` hook returns the attributes of a given participant and allows to set them.
9 * It requires a `Participant` object passed as property or via the `ParticipantContext`.
10 *
11 * @example
12 * ```tsx
13 * const { attributes } = useParticipantAttributes({ participant });
14 * ```
15 * @public
16 */
17export interface UseParticipantAttributesOptions {
18 participant?: Participant;
19}
20
21/** @public */
22export function useParticipantAttributes(props: UseParticipantAttributesOptions = {}) {
23 const participantContext = useMaybeParticipantContext();
24 const p = props.participant ?? participantContext;
25 const attributeObserver = React.useMemo(
26 // weird typescript constraint
27 () => (p ? participantAttributesObserver(p) : undefined),
28 [p],
29 );
30 const attributeState = useObservableState(attributeObserver, {
31 changed: {},
32 attributes: p?.attributes ?? {},
33 });
34
35 return attributeState;
36}
37
38/**
39 * The `useParticipantAttribute` hook returns the latest value of a given attribute key of a participant.
40 * It requires a `Participant` object passed as property in the `UseParticipantAttributesOptions` or via the `ParticipantContext`.
41 *
42 * @example
43 * ```tsx
44 * const myAttributeValue = useParticipantAttribute('targetAttributeName');
45 * ```
46 * @public
47 */
48export function useParticipantAttribute(
49 attributeKey: string,
50 options: UseParticipantAttributesOptions = {},
51) {
52 const p = useEnsureParticipant(options.participant);
53 const [attribute, setAttribute] = React.useState(p.attributes[attributeKey]);
54
55 React.useEffect(() => {
56 if (!p) {
57 return;
58 }
59 const subscription = participantAttributesObserver(p).subscribe((val) => {
60 if (val.changed[attributeKey] !== undefined) {
61 setAttribute(val.attributes[attributeKey]);
62 }
63 });
64 return () => {
65 subscription.unsubscribe();
66 };
67 }, [p, attributeKey]);
68
69 return attribute;
70}
Added view/app/src/vendor/livekit/react/hooks/useParticipantInfo.ts view/app/src/vendor/livekit/react/hooks/useParticipantInfo.ts +35 −0 View view/app/src/vendor/livekit/react/hooks/useParticipantInfo.ts
@@ -0,0 +1,35 @@
1import { participantInfoObserver } from '@livekit/components-core';
2import type { Participant } from 'livekit-client';
3import * as React from 'react';
4import { useMaybeParticipantContext } from '../context';
5import { useObservableState } from './internal';
6
7/**
8 * The `useParticipantInfo` hook returns the identity, name, and metadata of a given participant.
9 * It requires a `Participant` object passed as property or via the `ParticipantContext`.
10 *
11 * @example
12 * ```tsx
13 * const { identity, name, metadata } = useParticipantInfo({ participant });
14 * ```
15 * @public
16 */
17export interface UseParticipantInfoOptions {
18 participant?: Participant;
19}
20
21/** @public */
22export function useParticipantInfo(props: UseParticipantInfoOptions = {}) {
23 let p = useMaybeParticipantContext();
24 if (props.participant) {
25 p = props.participant;
26 }
27 const infoObserver = React.useMemo(() => participantInfoObserver(p), [p]);
28 const { identity, name, metadata } = useObservableState(infoObserver, {
29 name: p?.name,
30 identity: p?.identity,
31 metadata: p?.metadata,
32 });
33
34 return { identity, name, metadata };
35}
Added view/app/src/vendor/livekit/react/hooks/useParticipantPermissions.ts view/app/src/vendor/livekit/react/hooks/useParticipantPermissions.ts +29 −0 View view/app/src/vendor/livekit/react/hooks/useParticipantPermissions.ts
@@ -0,0 +1,29 @@
1import { participantPermissionObserver } from '@livekit/components-core';
2import type { ParticipantPermission } from '@livekit/protocol';
3import type { Participant } from 'livekit-client';
4import * as React from 'react';
5import { useEnsureParticipant } from '../context';
6import { useObservableState } from './internal/useObservableState';
7
8/**
9 * The `useParticipantPermissions` hook returns the permissions of a given participant.
10 *
11 * @example
12 * ```tsx
13 * const permissions = useParticipantPermissions({ participant });
14 * ```
15 * @public
16 */
17export interface UseParticipantPermissionsOptions {
18 participant?: Participant;
19}
20
21/** @public */
22export function useParticipantPermissions(
23 options: UseParticipantPermissionsOptions = {},
24): ParticipantPermission | undefined {
25 const p = useEnsureParticipant(options.participant);
26 const permissionObserver = React.useMemo(() => participantPermissionObserver(p), [p]);
27 const permissions = useObservableState(permissionObserver, p.permissions);
28 return permissions;
29}
Added view/app/src/vendor/livekit/react/hooks/useParticipants.ts view/app/src/vendor/livekit/react/hooks/useParticipants.ts +42 −0 View view/app/src/vendor/livekit/react/hooks/useParticipants.ts
@@ -0,0 +1,42 @@
1import type { Room, RoomEvent } from 'livekit-client';
2import { useLocalParticipant } from './useLocalParticipant';
3import { useRemoteParticipants } from './useRemoteParticipants';
4import * as React from 'react';
5
6/** @public */
7export interface UseParticipantsOptions {
8 /**
9 * To optimize performance, you can use the `updateOnlyOn` property to decide on what RoomEvents the hook updates.
10 * By default it updates on all relevant RoomEvents to keep the returned participants array up to date.
11 * The minimal set of non-overwriteable `RoomEvents` is: `[RoomEvent.ParticipantConnected, RoomEvent.ParticipantDisconnected, RoomEvent.ConnectionStateChanged]`
12 */
13 updateOnlyOn?: RoomEvent[];
14 /**
15 * The room to use. If not provided, the hook will use the room from the context.
16 */
17 room?: Room;
18}
19
20/**
21 * The `useParticipants` hook returns all participants (local and remote) of the current room.
22 * @remarks
23 * To optimize performance, you can use the `updateOnlyOn` property to decide on what `RoomEvents` the hook updates.
24 *
25 * @example
26 * ```tsx
27 * const participants = useParticipants();
28 * <ParticipantLoop participants={participants}>
29 * <ParticipantName />
30 * </ParticipantLoop>
31 * ```
32 * @public
33 */
34export function useParticipants(options: UseParticipantsOptions = {}) {
35 const remoteParticipants = useRemoteParticipants(options);
36 const { localParticipant } = useLocalParticipant(options);
37
38 return React.useMemo(
39 () => [localParticipant, ...remoteParticipants],
40 [localParticipant, remoteParticipants],
41 );
42}
Added view/app/src/vendor/livekit/react/hooks/useParticipantTile.ts view/app/src/vendor/livekit/react/hooks/useParticipantTile.ts +81 −0 View view/app/src/vendor/livekit/react/hooks/useParticipantTile.ts
@@ -0,0 +1,81 @@
1import type { ParticipantClickEvent, TrackReferenceOrPlaceholder } from '@livekit/components-core';
2import { setupParticipantTile } from '@livekit/components-core';
3import * as React from 'react';
4import { useEnsureTrackRef } from '../context';
5import { mergeProps } from '../mergeProps';
6import { useFacingMode } from './useFacingMode';
7import { useIsMuted } from './useIsMuted';
8import { useIsSpeaking } from './useIsSpeaking';
9import { Track } from 'livekit-client';
10
11/** @public */
12export interface UseParticipantTileProps<T extends HTMLElement> extends React.HTMLAttributes<T> {
13 /** The track reference to display. */
14 trackRef?: TrackReferenceOrPlaceholder;
15 disableSpeakingIndicator?: boolean;
16 onParticipantClick?: (event: ParticipantClickEvent) => void;
17 htmlProps: React.HTMLAttributes<T>;
18}
19
20/**
21 * The `useParticipantTile` hook is used to implement the `ParticipantTile` and returns the props needed to render the tile.
22 * @remarks
23 * The returned props include many data attributes that are useful for CSS styling purposes because they
24 * indicate the state of the participant and the track.
25 * For example: `data-lk-audio-muted`, `data-lk-video-muted`, `data-lk-speaking`, `data-lk-local-participant`, `data-lk-source`, `data-lk-facing-mode`.
26 * @public
27 */
28export function useParticipantTile<T extends HTMLElement>({
29 trackRef,
30 onParticipantClick,
31 disableSpeakingIndicator,
32 htmlProps,
33}: UseParticipantTileProps<T>) {
34 const trackReference = useEnsureTrackRef(trackRef);
35
36 const mergedProps = React.useMemo(() => {
37 const { className } = setupParticipantTile();
38 return mergeProps(htmlProps, {
39 className,
40 onClick: (event: React.MouseEvent<T, MouseEvent>) => {
41 htmlProps.onClick?.(event);
42 if (typeof onParticipantClick === 'function') {
43 const track =
44 trackReference.publication ??
45 trackReference.participant.getTrackPublication(trackReference.source);
46 onParticipantClick({ participant: trackReference.participant, track });
47 }
48 },
49 });
50 }, [
51 htmlProps,
52 onParticipantClick,
53 trackReference.publication,
54 trackReference.source,
55 trackReference.participant,
56 ]);
57
58 const micTrack = trackReference.participant.getTrackPublication(Track.Source.Microphone);
59 const micRef = React.useMemo(() => {
60 return {
61 participant: trackReference.participant,
62 source: Track.Source.Microphone,
63 publication: micTrack,
64 };
65 }, [micTrack, trackReference.participant]);
66 const isVideoMuted = useIsMuted(trackReference);
67 const isAudioMuted = useIsMuted(micRef);
68 const isSpeaking = useIsSpeaking(trackReference.participant);
69 const facingMode = useFacingMode(trackReference);
70 return {
71 elementProps: {
72 'data-lk-audio-muted': isAudioMuted,
73 'data-lk-video-muted': isVideoMuted,
74 'data-lk-speaking': disableSpeakingIndicator === true ? false : isSpeaking,
75 'data-lk-local-participant': trackReference.participant.isLocal,
76 'data-lk-source': trackReference.source,
77 'data-lk-facing-mode': facingMode,
78 ...mergedProps,
79 } as React.HTMLAttributes<T>,
80 };
81}
Added view/app/src/vendor/livekit/react/hooks/useParticipantTracks.ts view/app/src/vendor/livekit/react/hooks/useParticipantTracks.ts +53 −0 View view/app/src/vendor/livekit/react/hooks/useParticipantTracks.ts
@@ -0,0 +1,53 @@
1import * as React from 'react';
2import type { TrackReference } from '@livekit/components-core';
3import { participantTracksObservable } from '@livekit/components-core';
4import { useObservableState } from './internal';
5import type { Room, Track } from 'livekit-client';
6import { useMaybeParticipantContext } from '../context';
7import { useParticipants } from './useParticipants';
8
9type UseParticipantTracksOptions = {
10 participantIdentity?: string;
11 room?: Room;
12};
13
14/**
15 * `useParticipantTracks` is a custom React that allows you to get tracks of a specific participant only, by specifiying the participant's identity.
16 * If the participant identity is not passed the hook will try to get the participant from a participant context.
17 * @public
18 */
19export function useParticipantTracks<TrackSource extends Track.Source>(
20 sources: Array<TrackSource>,
21 optionsOrParticipantIdentity:
22 UseParticipantTracksOptions | UseParticipantTracksOptions['participantIdentity'] = {},
23): Array<TrackReference> {
24 let participantIdentity: UseParticipantTracksOptions['participantIdentity'];
25 let room: UseParticipantTracksOptions['room'];
26 if (typeof optionsOrParticipantIdentity === 'string') {
27 participantIdentity = optionsOrParticipantIdentity;
28 } else {
29 participantIdentity = optionsOrParticipantIdentity?.participantIdentity;
30 room = optionsOrParticipantIdentity?.room;
31 }
32
33 const participantContext = useMaybeParticipantContext();
34 const participants = useParticipants({ room, updateOnlyOn: [] });
35
36 const p = React.useMemo(() => {
37 if (participantIdentity) {
38 return participants.find((p) => p.identity === participantIdentity);
39 }
40 return participantContext;
41 }, [participantIdentity, participants, participantContext]);
42
43 const observable = React.useMemo(() => {
44 if (!p) {
45 return undefined;
46 }
47 return participantTracksObservable(p, { sources });
48 }, [p, JSON.stringify(sources)]);
49
50 const trackRefs = useObservableState(observable, [] as Array<TrackReference>);
51
52 return trackRefs;
53}
Added view/app/src/vendor/livekit/react/hooks/usePersistentUserChoices.ts view/app/src/vendor/livekit/react/hooks/usePersistentUserChoices.ts +64 −0 View view/app/src/vendor/livekit/react/hooks/usePersistentUserChoices.ts
@@ -0,0 +1,64 @@
1import type { LocalUserChoices } from '@livekit/components-core';
2import { loadUserChoices, saveUserChoices } from '@livekit/components-core';
3import * as React from 'react';
4
5/**
6 * Options for the `usePersistentDeviceSettings` hook.
7 * @alpha
8 */
9export interface UsePersistentUserChoicesOptions {
10 /**
11 * The default value to use if reading from local storage returns no results or fails.
12 */
13 defaults?: Partial<LocalUserChoices>;
14 /**
15 * Whether to prevent saving to persistent storage.
16 * @defaultValue false
17 */
18 preventSave?: boolean;
19 /**
20 * Whether to prevent loading user choices from persistent storage and use `defaults` instead.
21 * @defaultValue false
22 */
23 preventLoad?: boolean;
24}
25
26/**
27 * A hook that provides access to user choices stored in local storage, such as
28 * selected media devices and their current state (on or off), as well as the user name.
29 * @alpha
30 */
31export function usePersistentUserChoices(options: UsePersistentUserChoicesOptions = {}) {
32 const [userChoices, setSettings] = React.useState<LocalUserChoices>(
33 loadUserChoices(options.defaults, options.preventLoad ?? false),
34 );
35
36 const saveAudioInputEnabled = React.useCallback((isEnabled: boolean) => {
37 setSettings((prev) => ({ ...prev, audioEnabled: isEnabled }));
38 }, []);
39 const saveVideoInputEnabled = React.useCallback((isEnabled: boolean) => {
40 setSettings((prev) => ({ ...prev, videoEnabled: isEnabled }));
41 }, []);
42 const saveAudioInputDeviceId = React.useCallback((deviceId: string) => {
43 setSettings((prev) => ({ ...prev, audioDeviceId: deviceId }));
44 }, []);
45 const saveVideoInputDeviceId = React.useCallback((deviceId: string) => {
46 setSettings((prev) => ({ ...prev, videoDeviceId: deviceId }));
47 }, []);
48 const saveUsername = React.useCallback((username: string) => {
49 setSettings((prev) => ({ ...prev, username: username }));
50 }, []);
51
52 React.useEffect(() => {
53 saveUserChoices(userChoices, options.preventSave ?? false);
54 }, [userChoices, options.preventSave]);
55
56 return {
57 userChoices,
58 saveAudioInputEnabled,
59 saveVideoInputEnabled,
60 saveAudioInputDeviceId,
61 saveVideoInputDeviceId,
62 saveUsername,
63 };
64}
Added view/app/src/vendor/livekit/react/hooks/usePinnedTracks.ts view/app/src/vendor/livekit/react/hooks/usePinnedTracks.ts +24 −0 View view/app/src/vendor/livekit/react/hooks/usePinnedTracks.ts
@@ -0,0 +1,24 @@
1import type { TrackReferenceOrPlaceholder } from '@livekit/components-core';
2import * as React from 'react';
3import type { LayoutContextType } from '../context';
4import { useEnsureLayoutContext } from '../context';
5
6/**
7 * The `usePinnedTracks` hook returns a array of the pinned tracks of the current room.
8 * @remarks
9 * To function properly, this hook must be called within a `LayoutContext`.
10 * @example
11 * ```tsx
12 * const pinnedTracks = usePinnedTracks();
13 * ```
14 * @public
15 */
16export function usePinnedTracks(layoutContext?: LayoutContextType): TrackReferenceOrPlaceholder[] {
17 layoutContext = useEnsureLayoutContext(layoutContext);
18 return React.useMemo(() => {
19 if (layoutContext?.pin.state !== undefined && layoutContext.pin.state.length >= 1) {
20 return layoutContext.pin.state;
21 }
22 return [];
23 }, [layoutContext.pin.state]);
24}
Added view/app/src/vendor/livekit/react/hooks/useRemoteParticipant.ts view/app/src/vendor/livekit/react/hooks/useRemoteParticipant.ts +79 −0 View view/app/src/vendor/livekit/react/hooks/useRemoteParticipant.ts
@@ -0,0 +1,79 @@
1import {
2 type ParticipantIdentifier,
3 connectedParticipantObserver,
4 participantByIdentifierObserver,
5} from '@livekit/components-core';
6import type { ParticipantEvent, RemoteParticipant } from 'livekit-client';
7import * as React from 'react';
8import { useRoomContext } from '../context';
9
10/** @public */
11export interface UseRemoteParticipantOptions {
12 /**
13 * To optimize performance, you can use the `updateOnlyOn` property to decide on what `ParticipantEvents` the hook updates.
14 * By default it updates on all relevant ParticipantEvents to keep the returned participant up to date.
15 */
16 updateOnlyOn?: ParticipantEvent[];
17}
18
19/**
20 * The `useRemoteParticipant` hook returns the first RemoteParticipant by either identity and/or based on the participant kind.
21 * @remarks
22 * To optimize performance, you can use the `updateOnlyOn` property to decide on what `ParticipantEvents` the hook updates.
23 *
24 * @example
25 * ```tsx
26 * const participant = useRemoteParticipant({kind: ParticipantKind.Agent, identity: 'myAgent'});
27 * ```
28 * @public
29 */
30export function useRemoteParticipant(
31 identifier: ParticipantIdentifier,
32 options?: UseRemoteParticipantOptions,
33): RemoteParticipant | undefined;
34/**
35 * The `useRemoteParticipant` hook returns the first RemoteParticipant by either identity or based on the participant kind.
36 * @remarks
37 * To optimize performance, you can use the `updateOnlyOn` property to decide on what `ParticipantEvents` the hook updates.
38 *
39 * @example
40 * ```tsx
41 * const participant = useRemoteParticipant('Russ');
42 * ```
43 * @public
44 */
45export function useRemoteParticipant(
46 identity: string,
47 options?: UseRemoteParticipantOptions,
48): RemoteParticipant | undefined;
49export function useRemoteParticipant(
50 identityOrIdentifier: string | ParticipantIdentifier,
51 options: UseRemoteParticipantOptions = {},
52): RemoteParticipant | undefined {
53 const room = useRoomContext();
54 const [updateOnlyOn] = React.useState(options.updateOnlyOn);
55
56 const observable = React.useMemo(() => {
57 if (typeof identityOrIdentifier === 'string') {
58 return connectedParticipantObserver(room, identityOrIdentifier, {
59 additionalEvents: updateOnlyOn,
60 });
61 } else {
62 return participantByIdentifierObserver(room, identityOrIdentifier, {
63 additionalEvents: updateOnlyOn,
64 });
65 }
66 }, [room, JSON.stringify(identityOrIdentifier), updateOnlyOn]);
67
68 // Using `wrapperParticipant` to ensure a new object reference,
69 // triggering a re-render when the participant events fire.
70 const [participantWrapper, setParticipantWrapper] = React.useState({
71 p: undefined as RemoteParticipant | undefined,
72 });
73 React.useEffect(() => {
74 const listener = observable.subscribe((p) => setParticipantWrapper({ p }));
75 return () => listener.unsubscribe();
76 }, [observable]);
77
78 return participantWrapper.p;
79}
Added view/app/src/vendor/livekit/react/hooks/useRemoteParticipants.ts view/app/src/vendor/livekit/react/hooks/useRemoteParticipants.ts +45 −0 View view/app/src/vendor/livekit/react/hooks/useRemoteParticipants.ts
@@ -0,0 +1,45 @@
1import { connectedParticipantsObserver } from '@livekit/components-core';
2import type { RoomEvent, RemoteParticipant, Room } from 'livekit-client';
3import * as React from 'react';
4import { useEnsureRoom } from '../context';
5
6/** @public */
7export interface UseRemoteParticipantsOptions {
8 /**
9 * To optimize performance, you can use the `updateOnlyOn` property to decide on what RoomEvents the hook updates.
10 * By default it updates on all relevant RoomEvents to keep the returned participants array up to date.
11 * The minimal set of non-overwriteable `RoomEvents` is: `[RoomEvent.ParticipantConnected, RoomEvent.ParticipantDisconnected, RoomEvent.ConnectionStateChanged]`
12 */
13 updateOnlyOn?: RoomEvent[];
14 /**
15 * The room to use. If not provided, the hook will use the room from the context.
16 */
17 room?: Room;
18}
19
20/**
21 * The `useRemoteParticipants` hook returns all remote participants (without the local) of the current room.
22 * @remarks
23 * To optimize performance, you can use the `updateOnlyOn` property to decide on what `RoomEvents` the hook updates.
24 *
25 * @example
26 * ```tsx
27 * const participants = useRemoteParticipants();
28 * <ParticipantLoop participants={participants}>
29 * <ParticipantName />
30 * </ParticipantLoop>
31 * ```
32 * @public
33 */
34export function useRemoteParticipants(options: UseRemoteParticipantsOptions = {}) {
35 const room = useEnsureRoom(options.room);
36 const [participants, setParticipants] = React.useState<RemoteParticipant[]>([]);
37
38 React.useEffect(() => {
39 const listener = connectedParticipantsObserver(room, {
40 additionalRoomEvents: options.updateOnlyOn,
41 }).subscribe(setParticipants);
42 return () => listener.unsubscribe();
43 }, [room, JSON.stringify(options.updateOnlyOn)]);
44 return participants;
45}
Added view/app/src/vendor/livekit/react/hooks/useRoomInfo.ts view/app/src/vendor/livekit/react/hooks/useRoomInfo.ts +32 −0 View view/app/src/vendor/livekit/react/hooks/useRoomInfo.ts
@@ -0,0 +1,32 @@
1import { roomInfoObserver } from '@livekit/components-core';
2import type { Room } from 'livekit-client';
3import * as React from 'react';
4import { useEnsureRoom } from '../context';
5import { useObservableState } from './internal';
6
7/**
8 * The `useRoomInfo` hook returns the name and metadata of the given `Room`.
9 * @remarks
10 * Needs to be called inside a `RoomContext` or by passing a `Room` instance.
11 *
12 * @example
13 * ```tsx
14 * const { name, metadata } = useRoomInfo();
15 * ```
16 * @public
17 */
18export interface UseRoomInfoOptions {
19 room?: Room;
20}
21
22/** @public */
23export function useRoomInfo(options: UseRoomInfoOptions = {}) {
24 const room = useEnsureRoom(options.room);
25 const infoObserver = React.useMemo(() => roomInfoObserver(room), [room]);
26 const { name, metadata } = useObservableState(infoObserver, {
27 name: room.name,
28 metadata: room.metadata,
29 });
30
31 return { name, metadata };
32}
Added view/app/src/vendor/livekit/react/hooks/useSettingsToggle.ts view/app/src/vendor/livekit/react/hooks/useSettingsToggle.ts +32 −0 View view/app/src/vendor/livekit/react/hooks/useSettingsToggle.ts
@@ -0,0 +1,32 @@
1import { useLayoutContext } from '../context';
2import { mergeProps } from '../mergeProps';
3import * as React from 'react';
4
5/** @alpha */
6export interface UseSettingsToggleProps {
7 props: React.ButtonHTMLAttributes<HTMLButtonElement>;
8}
9
10/**
11 * The `useSettingsToggle` hook provides state and functions for toggling the settings menu.
12 * @remarks
13 * Depends on the `LayoutContext` to work properly.
14 * @see {@link SettingsMenu}
15 * @alpha
16 */
17export function useSettingsToggle({ props }: UseSettingsToggleProps) {
18 const { dispatch, state } = useLayoutContext().widget;
19 const className = 'lk-button lk-settings-toggle';
20
21 const mergedProps = React.useMemo(() => {
22 return mergeProps(props, {
23 className,
24 onClick: () => {
25 if (dispatch) dispatch({ msg: 'toggle_settings' });
26 },
27 'aria-pressed': state?.showSettings ? 'true' : 'false',
28 });
29 }, [props, className, dispatch, state]);
30
31 return { mergedProps };
32}
Added view/app/src/vendor/livekit/react/hooks/useSortedParticipants.ts view/app/src/vendor/livekit/react/hooks/useSortedParticipants.ts +20 −0 View view/app/src/vendor/livekit/react/hooks/useSortedParticipants.ts
@@ -0,0 +1,20 @@
1import { sortParticipants } from '@livekit/components-core';
2import type { Participant } from 'livekit-client';
3import * as React from 'react';
4import { useSpeakingParticipants } from './useSpeakingParticipants';
5
6/**
7 * The `useSortedParticipants` hook returns the participants sorted by importance.
8 * @public
9 */
10export function useSortedParticipants(participants: Array<Participant>) {
11 const [sortedParticipants, setSortedParticipants] = React.useState(
12 sortParticipants(participants),
13 );
14 const activeSpeakers = useSpeakingParticipants();
15
16 React.useEffect(() => {
17 setSortedParticipants(sortParticipants(participants));
18 }, [activeSpeakers, participants]);
19 return sortedParticipants;
20}
Added view/app/src/vendor/livekit/react/hooks/useSpeakingParticipants.ts view/app/src/vendor/livekit/react/hooks/useSpeakingParticipants.ts +27 −0 View view/app/src/vendor/livekit/react/hooks/useSpeakingParticipants.ts
@@ -0,0 +1,27 @@
1import { activeSpeakerObserver } from '@livekit/components-core';
2import * as React from 'react';
3import { useEnsureRoom } from '../context';
4import { useObservableState } from './internal';
5import { Room } from 'livekit-client';
6
7/** @public */
8export type UseSpeakingParticipantsOptions = {
9 room?: Room;
10};
11
12/**
13 * The `useSpeakingParticipants` hook returns only the active speakers of all participants.
14 *
15 * @example
16 * ```tsx
17 * const activeSpeakers = useSpeakingParticipants();
18 * ```
19 * @public
20 */
21export function useSpeakingParticipants(options?: UseSpeakingParticipantsOptions) {
22 const ensuredRoom = useEnsureRoom(options?.room);
23
24 const speakerObserver = React.useMemo(() => activeSpeakerObserver(ensuredRoom), [ensuredRoom]);
25 const activeSpeakers = useObservableState(speakerObserver, ensuredRoom.activeSpeakers);
26 return activeSpeakers;
27}
Added view/app/src/vendor/livekit/react/hooks/useStartAudio.ts view/app/src/vendor/livekit/react/hooks/useStartAudio.ts +50 −0 View view/app/src/vendor/livekit/react/hooks/useStartAudio.ts
@@ -0,0 +1,50 @@
1import { setupStartAudio } from '@livekit/components-core';
2import type { Room } from 'livekit-client';
3import * as React from 'react';
4import { useEnsureRoom } from '../context';
5import { mergeProps } from '../mergeProps';
6import { useObservableState } from './internal';
7
8/** @alpha */
9export interface UseStartAudioProps {
10 room?: Room;
11 props: React.ButtonHTMLAttributes<HTMLButtonElement>;
12}
13
14/**
15 * In many browsers to start audio playback, the user must perform a user-initiated event such as clicking a button.
16 * The `useStatAudio` hook returns an object with a boolean `canPlayAudio` flag
17 * that indicates whether audio playback is allowed in the current context,
18 * as well as a `startAudio` function that can be called in a button `onClick` callback to start audio playback in the current context.
19 *
20 * @see Autoplay policy on MDN web docs for more info: {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy}
21 * @alpha
22 */
23export function useStartAudio({ room, props }: UseStartAudioProps) {
24 const roomEnsured = useEnsureRoom(room);
25 const { className, roomAudioPlaybackAllowedObservable, handleStartAudioPlayback } = React.useMemo(
26 () => setupStartAudio(),
27 [],
28 );
29 const observable = React.useMemo(
30 () => roomAudioPlaybackAllowedObservable(roomEnsured),
31 [roomEnsured, roomAudioPlaybackAllowedObservable],
32 );
33 const { canPlayAudio } = useObservableState(observable, {
34 canPlayAudio: roomEnsured.canPlaybackAudio,
35 });
36
37 const mergedProps = React.useMemo(
38 () =>
39 mergeProps(props, {
40 className,
41 onClick: () => {
42 handleStartAudioPlayback(roomEnsured);
43 },
44 style: { display: canPlayAudio ? 'none' : 'block' },
45 }),
46 [props, className, canPlayAudio, handleStartAudioPlayback, roomEnsured],
47 );
48
49 return { mergedProps, canPlayAudio };
50}
Added view/app/src/vendor/livekit/react/hooks/useStartVideo.ts view/app/src/vendor/livekit/react/hooks/useStartVideo.ts +49 −0 View view/app/src/vendor/livekit/react/hooks/useStartVideo.ts
@@ -0,0 +1,49 @@
1import { setupStartVideo } from '@livekit/components-core';
2import type { Room } from 'livekit-client';
3import * as React from 'react';
4import { useEnsureRoom } from '../context';
5import { mergeProps } from '../mergeProps';
6import { useObservableState } from './internal';
7
8/** @alpha */
9export interface UseStartVideoProps {
10 room?: Room;
11 props: React.ButtonHTMLAttributes<HTMLButtonElement>;
12}
13
14/**
15 * In some browsers to start video playback in low power mode, the user must perform a user-initiated event such as clicking a button.
16 * The `useStartVideo` hook returns an object with a boolean `canPlayVideo` flag
17 * that indicates whether video playback is allowed in the current context,
18 * as well as a `startVideo` function that can be called in a button `onClick` callback to start video playback in the current context.
19 *
20 * @alpha
21 */
22export function useStartVideo({ room, props }: UseStartVideoProps) {
23 const roomEnsured = useEnsureRoom(room);
24 const { className, roomVideoPlaybackAllowedObservable, handleStartVideoPlayback } = React.useMemo(
25 () => setupStartVideo(),
26 [],
27 );
28 const observable = React.useMemo(
29 () => roomVideoPlaybackAllowedObservable(roomEnsured),
30 [roomEnsured, roomVideoPlaybackAllowedObservable],
31 );
32 const { canPlayVideo } = useObservableState(observable, {
33 canPlayVideo: roomEnsured.canPlaybackVideo,
34 });
35
36 const mergedProps = React.useMemo(
37 () =>
38 mergeProps(props, {
39 className,
40 onClick: () => {
41 handleStartVideoPlayback(roomEnsured);
42 },
43 style: { display: canPlayVideo ? 'none' : 'block' },
44 }),
45 [props, className, canPlayVideo, handleStartVideoPlayback, roomEnsured],
46 );
47
48 return { mergedProps, canPlayVideo };
49}
Added view/app/src/vendor/livekit/react/hooks/useSwipe.ts view/app/src/vendor/livekit/react/hooks/useSwipe.ts +68 −0 View view/app/src/vendor/livekit/react/hooks/useSwipe.ts
@@ -0,0 +1,68 @@
1import * as React from 'react';
2
3/**
4 * @alpha
5 */
6export type UseSwipeOptions = {
7 minSwipeDistance?: number;
8 onLeftSwipe?: () => void;
9 onRightSwipe?: () => void;
10};
11
12/**
13 * Simple implementation to detect horizontal swipe actions.
14 * Accepts callbacks for on right and left swipes.
15 * @example
16 * ```tsx
17 * <div
18 onTouchStart={onTouchStart}
19 onTouchMove={onTouchMove}
20 onTouchEnd={onTouchEnd}
21 >
22 * ```
23 * @alpha
24 */
25export function useSwipe(element: React.RefObject<HTMLElement | null>, options: UseSwipeOptions = {}) {
26 const touchStart = React.useRef<number | null>(null);
27 const touchEnd = React.useRef<number | null>(null);
28
29 // The required distance between touchStart and touchEnd to be detected as a swipe.
30 const minSwipeDistance = options.minSwipeDistance ?? 50;
31
32 const onTouchStart = (event: TouchEvent) => {
33 touchEnd.current = null; // Otherwise the swipe is fired even with usual touch events.
34 touchStart.current = event.targetTouches[0].clientX;
35 };
36
37 const onTouchMove = (event: TouchEvent) => {
38 touchEnd.current = event.targetTouches[0].clientX;
39 };
40
41 const onTouchEnd = React.useCallback(() => {
42 if (!touchStart.current || !touchEnd.current) {
43 return;
44 }
45 const distance = touchStart.current - touchEnd.current;
46 const isLeftSwipe = distance > minSwipeDistance;
47 const isRightSwipe = distance < -minSwipeDistance;
48
49 if (isLeftSwipe && options.onLeftSwipe) options.onLeftSwipe();
50 if (isRightSwipe && options.onRightSwipe) options.onRightSwipe();
51 }, [minSwipeDistance, options]);
52
53 React.useEffect(() => {
54 const elementCopy = element.current;
55 if (elementCopy) {
56 elementCopy.addEventListener('touchstart', onTouchStart, { passive: true });
57 elementCopy.addEventListener('touchmove', onTouchMove, { passive: true });
58 elementCopy.addEventListener('touchend', onTouchEnd, { passive: true });
59 }
60 return () => {
61 if (elementCopy) {
62 elementCopy.removeEventListener('touchstart', onTouchStart);
63 elementCopy.removeEventListener('touchmove', onTouchMove);
64 elementCopy.removeEventListener('touchend', onTouchEnd);
65 }
66 };
67 }, [element, onTouchEnd]);
68}
Added view/app/src/vendor/livekit/react/hooks/useToken.ts view/app/src/vendor/livekit/react/hooks/useToken.ts +54 −0 View view/app/src/vendor/livekit/react/hooks/useToken.ts
@@ -0,0 +1,54 @@
1import { log } from '@livekit/components-core';
2import * as React from 'react';
3
4/** @public */
5export interface UserInfo {
6 identity?: string;
7 name?: string;
8 metadata?: string;
9}
10
11/** @public */
12export interface UseTokenOptions {
13 userInfo?: UserInfo;
14}
15
16/**
17 * The `useToken` hook fetches a token from the given token endpoint with the given user info.
18 *
19 * @example
20 * ```tsx
21 * const token = useToken(<token-endpoint>, roomName, { userInfo: { identity, name }});
22 * ```
23 * @public */
24export function useToken(
25 tokenEndpoint: string | undefined,
26 roomName: string,
27 options: UseTokenOptions = {},
28) {
29 const [token, setToken] = React.useState<string | undefined>(undefined);
30
31 React.useEffect(() => {
32 if (tokenEndpoint === undefined) {
33 throw Error('token endpoint needs to be defined');
34 }
35 if (options.userInfo?.identity === undefined) {
36 return;
37 }
38 const tokenFetcher = async () => {
39 log.debug('fetching token');
40 const params = new URLSearchParams({ ...options.userInfo, roomName });
41 const res = await fetch(`${tokenEndpoint}?${params.toString()}`);
42 if (!res.ok) {
43 log.error(
44 `Could not fetch token. Server responded with status ${res.status}: ${res.statusText}`,
45 );
46 return;
47 }
48 const { accessToken } = await res.json();
49 setToken(accessToken);
50 };
51 tokenFetcher();
52 }, [tokenEndpoint, roomName, JSON.stringify(options)]);
53 return token;
54}
Added view/app/src/vendor/livekit/react/hooks/useTrack.ts view/app/src/vendor/livekit/react/hooks/useTrack.ts +11 −0 View view/app/src/vendor/livekit/react/hooks/useTrack.ts
@@ -0,0 +1,11 @@
1// import type { Participant, Track } from 'livekit-client';
2// import { useTrackRefBySourceOrName } from './useTrackRefBySourceOrName';
3// import { useEnsureParticipant } from '../context';
4
5/**
6 * TODO decide whether we want to expose this
7 */
8// function useTrack(source: Track.Source, participant?: Participant) {
9// const p = useEnsureParticipant(participant);
10// return useTrackRefBySourceOrName({ source, participant: p });
11// }
Added view/app/src/vendor/livekit/react/hooks/useTrackByName.ts view/app/src/vendor/livekit/react/hooks/useTrackByName.ts +15 −0 View view/app/src/vendor/livekit/react/hooks/useTrackByName.ts
@@ -0,0 +1,15 @@
1import type { Participant } from 'livekit-client';
2import { useEnsureParticipant } from '../context';
3import { useTrackRefBySourceOrName } from './useTrackRefBySourceOrName';
4
5/**
6 * This function `useTrackByName` allows you to access a track by referencing its track name.
7 * Inside the function, it ensures that the a valid `participant` reference is available by checking
8 * for both a passed participant argument and, if not available, a valid participant context.
9 *
10 * @public
11 */
12export function useTrackByName(name: string, participant?: Participant) {
13 const p = useEnsureParticipant(participant);
14 return useTrackRefBySourceOrName({ name, participant: p });
15}
Added view/app/src/vendor/livekit/react/hooks/useTrackMutedIndicator.ts view/app/src/vendor/livekit/react/hooks/useTrackMutedIndicator.ts +44 −0 View view/app/src/vendor/livekit/react/hooks/useTrackMutedIndicator.ts
@@ -0,0 +1,44 @@
1import {
2 type TrackReferenceOrPlaceholder,
3 setupTrackMutedIndicator,
4 getTrackReferenceId,
5} from '@livekit/components-core';
6import * as React from 'react';
7import { useEnsureTrackRef } from '../context';
8import { useObservableState } from './internal';
9
10interface TrackMutedIndicatorReturnType {
11 isMuted: boolean;
12 className: string;
13}
14
15/**
16 * The `useTrackMutedIndicator` hook is used to implement the `TrackMutedIndicator` component
17 * and returns the muted state of the given track.
18 *
19 * @example
20 * ```tsx
21 * const { isMuted } = useTrackMutedIndicator(trackRef);
22 * ```
23 * @public
24 */
25export function useTrackMutedIndicator(
26 trackRef?: TrackReferenceOrPlaceholder,
27): TrackMutedIndicatorReturnType {
28 const trackReference = useEnsureTrackRef(trackRef);
29
30 const { className, mediaMutedObserver } = React.useMemo(
31 () => setupTrackMutedIndicator(trackReference),
32 [getTrackReferenceId(trackReference)],
33 );
34
35 const isMuted = useObservableState(
36 mediaMutedObserver,
37 !!(
38 trackReference.publication?.isMuted ||
39 trackReference.participant.getTrackPublication(trackReference.source)?.isMuted
40 ),
41 );
42
43 return { isMuted, className };
44}
Added view/app/src/vendor/livekit/react/hooks/useTrackRefBySourceOrName.ts view/app/src/vendor/livekit/react/hooks/useTrackRefBySourceOrName.ts +30 −0 View view/app/src/vendor/livekit/react/hooks/useTrackRefBySourceOrName.ts
@@ -0,0 +1,30 @@
1import type { TrackReferenceOrPlaceholder, TrackSource } from '@livekit/components-core';
2import { setupMediaTrack, getTrackByIdentifier } from '@livekit/components-core';
3import * as React from 'react';
4import { Track } from 'livekit-client';
5
6/**
7 * @internal
8 */
9export function useTrackRefBySourceOrName(
10 source: TrackSource<Track.Source>,
11): TrackReferenceOrPlaceholder {
12 const [publication, setPublication] = React.useState(getTrackByIdentifier(source));
13
14 const { trackObserver } = React.useMemo(() => {
15 return setupMediaTrack(source);
16 }, [source.participant.sid ?? source.participant.identity, source.source]);
17
18 React.useEffect(() => {
19 const subscription = trackObserver.subscribe((publication) => {
20 setPublication(publication);
21 });
22 return () => subscription?.unsubscribe();
23 }, [trackObserver]);
24
25 return {
26 participant: source.participant,
27 source: source.source ?? Track.Source.Unknown,
28 publication,
29 };
30}
Added view/app/src/vendor/livekit/react/hooks/useTracks.ts view/app/src/vendor/livekit/react/hooks/useTracks.ts +154 −0 View view/app/src/vendor/livekit/react/hooks/useTracks.ts
@@ -0,0 +1,154 @@
1import type {
2 SourcesArray,
3 TrackReference,
4 TrackReferenceOrPlaceholder,
5 TrackSourceWithOptions,
6 TrackReferencePlaceholder,
7} from '@livekit/components-core';
8import {
9 isSourcesWithOptions,
10 isSourceWitOptions,
11 log,
12 trackReferencesObservable,
13} from '@livekit/components-core';
14import type { Participant, Room, RoomEvent } from 'livekit-client';
15import { Track } from 'livekit-client';
16import * as React from 'react';
17import { useEnsureRoom } from '../context';
18
19/** @public */
20export type UseTracksOptions = {
21 updateOnlyOn?: RoomEvent[];
22 onlySubscribed?: boolean;
23 room?: Room;
24};
25
26/** @public */
27export type UseTracksHookReturnType<T> = T extends Track.Source[]
28 ? TrackReference[]
29 : T extends TrackSourceWithOptions[]
30 ? TrackReferenceOrPlaceholder[]
31 : never;
32
33/**
34 * The `useTracks` hook returns an array of `TrackReference` or `TrackReferenceOrPlaceholder` depending on the provided `sources` property.
35 * If only subscribed tracks are desired, set the `onlySubscribed` property to `true`.
36 * @example
37 * ```ts
38 * // Return all camera track publications.
39 * const trackReferences: TrackReference[] = useTracks([Track.Source.Camera])
40 * ```
41 * @example
42 * ```ts
43 * // Return all subscribed camera tracks as well as placeholders for
44 * // participants without a camera subscription.
45 * const trackReferencesWithPlaceholders: TrackReferenceOrPlaceholder[] = useTracks([{source: Track.Source.Camera, withPlaceholder: true}])
46 * ```
47 * @public
48 */
49export function useTracks<T extends SourcesArray = Track.Source[]>(
50 sources: T = [
51 Track.Source.Camera,
52 Track.Source.Microphone,
53 Track.Source.ScreenShare,
54 Track.Source.ScreenShareAudio,
55 Track.Source.Unknown,
56 ] as T,
57 options: UseTracksOptions = {},
58): UseTracksHookReturnType<T> {
59 const room = useEnsureRoom(options.room);
60 const [trackReferences, setTrackReferences] = React.useState<TrackReference[]>([]);
61 const [participants, setParticipants] = React.useState<Participant[]>([]);
62
63 const sources_ = React.useMemo(() => {
64 return sources.map((s) => (isSourceWitOptions(s) ? s.source : s));
65 }, [JSON.stringify(sources)]);
66
67 React.useEffect(() => {
68 const subscription = trackReferencesObservable(room, sources_, {
69 additionalRoomEvents: options.updateOnlyOn,
70 onlySubscribed: options.onlySubscribed,
71 }).subscribe(({ trackReferences, participants }) => {
72 log.debug('setting track bundles', trackReferences, participants);
73 setTrackReferences(trackReferences);
74 setParticipants(participants);
75 });
76 return () => subscription.unsubscribe();
77 }, [
78 room,
79 JSON.stringify(options.onlySubscribed),
80 JSON.stringify(options.updateOnlyOn),
81 JSON.stringify(sources),
82 ]);
83
84 const maybeTrackReferences = React.useMemo(() => {
85 if (isSourcesWithOptions(sources)) {
86 const requirePlaceholder = requiredPlaceholders(sources, participants);
87 const trackReferencesWithPlaceholders: TrackReferenceOrPlaceholder[] =
88 Array.from(trackReferences);
89 participants.forEach((participant) => {
90 if (requirePlaceholder.has(participant.identity)) {
91 const sourcesToAddPlaceholder = requirePlaceholder.get(participant.identity) ?? [];
92 sourcesToAddPlaceholder.forEach((placeholderSource) => {
93 if (
94 trackReferences.find(
95 ({ participant: p, publication }) =>
96 participant.identity === p.identity && publication.source === placeholderSource,
97 )
98 ) {
99 return;
100 }
101 log.debug(
102 `Add ${placeholderSource} placeholder for participant ${participant.identity}.`,
103 );
104 const placeholder: TrackReferencePlaceholder = {
105 participant,
106 source: placeholderSource,
107 };
108 trackReferencesWithPlaceholders.push(placeholder);
109 });
110 }
111 });
112 return trackReferencesWithPlaceholders;
113 } else {
114 return trackReferences;
115 }
116 }, [trackReferences, participants, sources]);
117
118 return maybeTrackReferences as UseTracksHookReturnType<T>;
119}
120
121function difference<T>(setA: Set<T>, setB: Set<T>): Set<T> {
122 const _difference = new Set(setA);
123 for (const elem of setB) {
124 _difference.delete(elem);
125 }
126 return _difference;
127}
128
129export function requiredPlaceholders<T extends SourcesArray>(
130 sources: T,
131 participants: Participant[],
132): Map<Participant['identity'], Track.Source[]> {
133 const placeholderMap = new Map<Participant['identity'], Track.Source[]>();
134 if (isSourcesWithOptions(sources)) {
135 const sourcesThatNeedPlaceholder = sources
136 .filter((sourceWithOption) => sourceWithOption.withPlaceholder)
137 .map((sourceWithOption) => sourceWithOption.source);
138
139 participants.forEach((participant) => {
140 const sourcesOfSubscribedTracks = participant
141 .getTrackPublications()
142 .map((pub) => pub.track?.source)
143 .filter((trackSource): trackSource is Track.Source => trackSource !== undefined);
144 const placeholderNeededForThisParticipant = Array.from(
145 difference(new Set(sourcesThatNeedPlaceholder), new Set(sourcesOfSubscribedTracks)),
146 );
147 // If the participant needs placeholder add it to the placeholder map.
148 if (placeholderNeededForThisParticipant.length > 0) {
149 placeholderMap.set(participant.identity, placeholderNeededForThisParticipant);
150 }
151 });
152 }
153 return placeholderMap;
154}
Added view/app/src/vendor/livekit/react/hooks/useTrackSyncTime.ts view/app/src/vendor/livekit/react/hooks/useTrackSyncTime.ts +18 −0 View view/app/src/vendor/livekit/react/hooks/useTrackSyncTime.ts
@@ -0,0 +1,18 @@
1import * as React from 'react';
2import { type TrackReferenceOrPlaceholder, trackSyncTimeObserver } from '@livekit/components-core';
3import { useObservableState } from './internal';
4
5/**
6 * @internal
7 * @deprecated this is an internal hook that's only used by deprecated APIs
8 */
9export function useTrackSyncTime(ref: TrackReferenceOrPlaceholder | undefined) {
10 const observable = React.useMemo(
11 () => (ref?.publication?.track ? trackSyncTimeObserver(ref?.publication.track) : undefined),
12 [ref?.publication?.track],
13 );
14 return useObservableState(observable, {
15 timestamp: Date.now(),
16 rtpTimestamp: ref?.publication?.track?.rtpTimestamp,
17 });
18}
Added view/app/src/vendor/livekit/react/hooks/useTrackToggle.ts view/app/src/vendor/livekit/react/hooks/useTrackToggle.ts +95 −0 View view/app/src/vendor/livekit/react/hooks/useTrackToggle.ts
@@ -0,0 +1,95 @@
1import type { ToggleSource } from '@livekit/components-core';
2import { setupMediaToggle, setupManualToggle, log } from '@livekit/components-core';
3import * as React from 'react';
4import { type Room } from 'livekit-client';
5import type { TrackToggleProps } from '../components';
6import { useMaybeRoomContext } from '../context';
7import { mergeProps } from '../mergeProps';
8import { useObservableState } from './internal';
9
10/** @public */
11export interface UseTrackToggleProps<T extends ToggleSource> extends Omit<
12 TrackToggleProps<T>,
13 'showIcon'
14> {
15 room?: Room;
16}
17
18/**
19 * The `useTrackToggle` hook is used to implement the `TrackToggle` component and returns state
20 * and functionality of the given track.
21 *
22 * @example
23 * ```tsx
24 * const { buttonProps, enabled } = useTrackToggle(trackRef);
25 * return <button {...buttonProps}>{enabled ? 'disable' : 'enable'}</button>;
26 * ```
27 * @public
28 */
29export function useTrackToggle<T extends ToggleSource>({
30 source,
31 onChange,
32 initialState,
33 captureOptions,
34 publishOptions,
35 onDeviceError,
36 room,
37 ...rest
38}: UseTrackToggleProps<T>) {
39 const roomFromContext = useMaybeRoomContext();
40 const roomFallback = React.useMemo(() => room ?? roomFromContext, [room, roomFromContext]);
41 const track = roomFallback?.localParticipant?.getTrackPublication(source);
42 /** `true` if a user interaction such as a click on the TrackToggle button has occurred. */
43 const userInteractionRef = React.useRef(false);
44
45 const { toggle, className, pendingObserver, enabledObserver } = React.useMemo(
46 () =>
47 roomFallback
48 ? setupMediaToggle<T>(source, roomFallback, captureOptions, publishOptions, onDeviceError)
49 : setupManualToggle(),
50 [roomFallback, source, JSON.stringify(captureOptions), publishOptions],
51 );
52
53 const pending = useObservableState(pendingObserver, false);
54 const enabled = useObservableState(enabledObserver, initialState ?? !!track?.isEnabled);
55
56 React.useEffect(() => {
57 onChange?.(enabled, userInteractionRef.current);
58 userInteractionRef.current = false;
59 }, [enabled, onChange]);
60
61 React.useEffect(() => {
62 if (initialState !== undefined) {
63 log.debug('forcing initial toggle state', source, initialState);
64 toggle(initialState);
65 }
66 // only execute once at the beginning
67 // eslint-disable-next-line react-hooks/exhaustive-deps
68 }, []);
69
70 const newProps = React.useMemo(() => mergeProps(rest, { className }), [rest, className]);
71
72 const clickHandler: React.MouseEventHandler<HTMLButtonElement> = React.useCallback(
73 (evt) => {
74 userInteractionRef.current = true;
75 toggle().catch(() => (userInteractionRef.current = false));
76 rest.onClick?.(evt);
77 },
78 [rest, toggle],
79 );
80
81 return {
82 toggle,
83 enabled,
84 pending,
85 track,
86 buttonProps: {
87 ...newProps,
88 'aria-pressed': enabled,
89 'data-lk-source': source,
90 'data-lk-enabled': enabled,
91 disabled: pending,
92 onClick: clickHandler,
93 } as React.ButtonHTMLAttributes<HTMLButtonElement>,
94 };
95}
Added view/app/src/vendor/livekit/react/hooks/useTrackVolume.ts view/app/src/vendor/livekit/react/hooks/useTrackVolume.ts +283 −0 View view/app/src/vendor/livekit/react/hooks/useTrackVolume.ts
@@ -0,0 +1,283 @@
1import * as React from 'react';
2import type { LocalAudioTrack, RemoteAudioTrack, AudioAnalyserOptions } from 'livekit-client';
3import { Track, createAudioAnalyser } from 'livekit-client';
4import {
5 type TrackReference,
6 isTrackReference,
7 type TrackReferenceOrPlaceholder,
8} from '@livekit/components-core';
9
10/**
11 * @alpha
12 * Hook for tracking the volume of an audio track using the Web Audio API.
13 */
14export function useTrackVolume(
15 trackOrTrackReference?: LocalAudioTrack | RemoteAudioTrack | TrackReference,
16 options: AudioAnalyserOptions = { fftSize: 32, smoothingTimeConstant: 0 },
17) {
18 const track = isTrackReference(trackOrTrackReference)
19 ? (trackOrTrackReference.publication.track as LocalAudioTrack | RemoteAudioTrack | undefined)
20 : trackOrTrackReference;
21 const [volume, setVolume] = React.useState(0);
22 React.useEffect(() => {
23 if (!track || !track.mediaStream) {
24 return;
25 }
26
27 const { cleanup, analyser } = createAudioAnalyser(track, options);
28
29 const bufferLength = analyser.frequencyBinCount;
30 const dataArray = new Uint8Array(bufferLength);
31
32 const updateVolume = () => {
33 analyser.getByteFrequencyData(dataArray);
34 let sum = 0;
35 for (let i = 0; i < dataArray.length; i++) {
36 const a = dataArray[i];
37 sum += a * a;
38 }
39 setVolume(Math.sqrt(sum / dataArray.length) / 255);
40 };
41
42 const interval = setInterval(updateVolume, 1000 / 30);
43
44 return () => {
45 cleanup();
46 clearInterval(interval);
47 };
48 }, [track, track?.mediaStream, JSON.stringify(options)]);
49
50 return volume;
51}
52
53const normalizeFrequencies = (frequencies: Float32Array) => {
54 const normalizeDb = (value: number) => {
55 const minDb = -100;
56 const maxDb = -10;
57 let db = 1 - (Math.max(minDb, Math.min(maxDb, value)) * -1) / 100;
58 db = Math.sqrt(db);
59
60 return db;
61 };
62
63 // Normalize all frequency values
64 return frequencies.map((value) => {
65 if (value === -Infinity) {
66 return 0;
67 }
68 return normalizeDb(value);
69 });
70};
71
72/**
73 * Interface for configuring options for the useMultibandTrackVolume hook.
74 * @alpha
75 */
76export interface MultiBandTrackVolumeOptions {
77 bands?: number;
78 /**
79 * cut off of frequency bins on the lower end
80 * Note: this is not a frequency measure, but in relation to analyserOptions.fftSize,
81 */
82 loPass?: number;
83 /**
84 * cut off of frequency bins on the higher end
85 * Note: this is not a frequency measure, but in relation to analyserOptions.fftSize,
86 */
87 hiPass?: number;
88 /**
89 * update should run every x ms
90 */
91 updateInterval?: number;
92 analyserOptions?: AnalyserOptions;
93}
94
95const multibandDefaults = {
96 bands: 5,
97 loPass: 100,
98 hiPass: 600,
99 updateInterval: 32,
100 analyserOptions: { fftSize: 2048 },
101} as const satisfies MultiBandTrackVolumeOptions;
102
103/**
104 * Hook for tracking the volume of an audio track across multiple frequency bands using the Web Audio API.
105 * @alpha
106 */
107export function useMultibandTrackVolume(
108 trackOrTrackReference?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder,
109 options: MultiBandTrackVolumeOptions = {},
110) {
111 const track =
112 trackOrTrackReference instanceof Track
113 ? trackOrTrackReference
114 : (trackOrTrackReference?.publication?.track as LocalAudioTrack | RemoteAudioTrack | undefined);
115 const opts = { ...multibandDefaults, ...options };
116 const [frequencyBands, setFrequencyBands] = React.useState<Array<number>>(
117 new Array(opts.bands).fill(0),
118 );
119
120 React.useEffect(() => {
121 if (!track || !track?.mediaStream) {
122 setFrequencyBands((val) => val.slice().fill(0));
123 return;
124 }
125 const { analyser, cleanup } = createAudioAnalyser(track, opts.analyserOptions);
126
127 const bufferLength = analyser.frequencyBinCount;
128 const dataArray = new Float32Array(bufferLength);
129
130 const updateVolume = () => {
131 analyser.getFloatFrequencyData(dataArray);
132 let frequencies: Float32Array = new Float32Array(dataArray.length);
133 for (let i = 0; i < dataArray.length; i++) {
134 frequencies[i] = dataArray[i];
135 }
136 frequencies = frequencies.slice(opts.loPass, opts.hiPass);
137
138 const normalizedFrequencies = normalizeFrequencies(frequencies); // is this needed ?
139 const totalBins = normalizedFrequencies.length;
140 const chunks: Array<number> = [];
141 for (let i = 0; i < opts.bands; i++) {
142 // Use proportional distribution to evenly divide bins across bands
143 const startIndex = Math.floor((i * totalBins) / opts.bands);
144 const endIndex = Math.floor(((i + 1) * totalBins) / opts.bands);
145 const chunk = normalizedFrequencies.slice(startIndex, endIndex);
146 const chunkLength = chunk.length;
147 if (chunkLength === 0) {
148 chunks.push(0);
149 } else {
150 const summedVolumes = chunk.reduce((acc, val) => (acc += val), 0);
151 chunks.push(summedVolumes / chunkLength);
152 }
153 }
154
155 setFrequencyBands(chunks);
156 };
157
158 const interval = setInterval(updateVolume, opts.updateInterval);
159
160 return () => {
161 cleanup();
162 clearInterval(interval);
163 };
164 }, [track, track?.mediaStream, JSON.stringify(options)]);
165
166 return frequencyBands;
167}
168
169/**
170 * @alpha
171 */
172export interface AudioWaveformOptions {
173 barCount?: number;
174 volMultiplier?: number;
175 updateInterval?: number;
176}
177
178const waveformDefaults = {
179 barCount: 120,
180 volMultiplier: 5,
181 updateInterval: 20,
182} as const satisfies AudioWaveformOptions;
183
184/**
185 * @alpha
186 */
187export function useAudioWaveform(
188 trackOrTrackReference?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder,
189 options: AudioWaveformOptions = {},
190) {
191 const track =
192 trackOrTrackReference instanceof Track
193 ? trackOrTrackReference
194 : (trackOrTrackReference?.publication?.track as LocalAudioTrack | RemoteAudioTrack | undefined);
195 const opts = { ...waveformDefaults, ...options };
196
197 const aggregateWave = React.useRef(new Float32Array());
198 const timeRef = React.useRef(performance.now());
199 const updates = React.useRef(0);
200 const [bars, setBars] = React.useState<number[]>([]);
201
202 const onUpdate = React.useCallback((wave: Float32Array) => {
203 setBars(
204 Array.from(
205 filterData(wave, opts.barCount).map((v) => Math.sqrt(v) * opts.volMultiplier),
206 // wave.slice(0, opts.barCount).map((v) => sigmoid(v * opts.volMultiplier, 0.08, 0.2)),
207 ),
208 );
209 }, []);
210
211 React.useEffect(() => {
212 if (!track || !track?.mediaStream) {
213 return;
214 }
215 const { analyser, cleanup } = createAudioAnalyser(track, {
216 fftSize: getFFTSizeValue(opts.barCount),
217 });
218
219 const bufferLength = getFFTSizeValue(opts.barCount);
220 const dataArray = new Float32Array(bufferLength);
221
222 const update = () => {
223 updateWaveform = requestAnimationFrame(update);
224 analyser.getFloatTimeDomainData(dataArray);
225 aggregateWave.current.map((v, i) => v + dataArray[i]);
226 updates.current += 1;
227
228 if (performance.now() - timeRef.current >= opts.updateInterval) {
229 const newData = dataArray.map((v) => v / updates.current);
230 onUpdate(newData);
231 timeRef.current = performance.now();
232 updates.current = 0;
233 }
234 };
235
236 let updateWaveform = requestAnimationFrame(update);
237
238 return () => {
239 cleanup();
240 cancelAnimationFrame(updateWaveform);
241 };
242 }, [track, track?.mediaStream, JSON.stringify(options), onUpdate]);
243
244 return {
245 bars,
246 };
247}
248
249function getFFTSizeValue(x: number) {
250 if (x < 32) return 32;
251 else return pow2ceil(x);
252}
253
254// function sigmoid(x: number, k = 2, s = 0) {
255// return 1 / (1 + Math.exp(-(x - s) / k));
256// }
257
258function pow2ceil(v: number) {
259 let p = 2;
260 while ((v >>= 1)) {
261 p <<= 1;
262 }
263 return p;
264}
265
266function filterData(audioData: Float32Array, numSamples: number) {
267 const blockSize = Math.floor(audioData.length / numSamples); // the number of samples in each subdivision
268 const filteredData = new Float32Array(numSamples);
269 for (let i = 0; i < numSamples; i++) {
270 const blockStart = blockSize * i; // the location of the first sample in the block
271 let sum = 0;
272 for (let j = 0; j < blockSize; j++) {
273 sum = sum + Math.abs(audioData[blockStart + j]); // find the sum of all the samples in the block
274 }
275 filteredData[i] = sum / blockSize; // divide the sum by the block size to get the average
276 }
277 return filteredData;
278}
279
280// function normalizeData(audioData: Float32Array) {
281// const multiplier = Math.pow(Math.max(...audioData), -1);
282// return audioData.map((n) => n * multiplier);
283// }
Added view/app/src/vendor/livekit/react/hooks/useVisualStableUpdate.ts view/app/src/vendor/livekit/react/hooks/useVisualStableUpdate.ts +63 −0 View view/app/src/vendor/livekit/react/hooks/useVisualStableUpdate.ts
@@ -0,0 +1,63 @@
1import type { TrackReferenceOrPlaceholder } from '@livekit/components-core';
2import { log, sortTrackReferences, updatePages } from '@livekit/components-core';
3import * as React from 'react';
4
5/** @public */
6export interface UseVisualStableUpdateOptions {
7 /** Overwrites the default sort function. */
8 customSortFunction?: (
9 trackReferences: TrackReferenceOrPlaceholder[],
10 ) => TrackReferenceOrPlaceholder[];
11}
12
13/**
14 * The `useVisualStableUpdate` hook is used to prevent visually jarring jumps and shifts of elements
15 * in an array. The algorithm only starts to update when there are more items than visually fit
16 * on a page. If this is the case, it will make sure that speaking participants move to the first
17 * page and are always visible.
18 * @remarks
19 * Updating the array can occur because attendees leave or join a room, or because they mute/unmute
20 * or start speaking.
21 * The hook is used for the `GridLayout` and `CarouselLayout` components.
22 *
23 * @example
24 * ```tsx
25 * const trackRefs = useTracks();
26 * const updatedTrackRefs = useVisualStableUpdate(trackRefs, itemPerPage);
27 * ```
28 * @public
29 */
30export function useVisualStableUpdate(
31 /** `TrackReference`s to display in the grid. */
32 trackReferences: TrackReferenceOrPlaceholder[],
33 maxItemsOnPage: number,
34 options: UseVisualStableUpdateOptions = {},
35): TrackReferenceOrPlaceholder[] {
36 const lastTrackRefs = React.useRef<TrackReferenceOrPlaceholder[]>([]);
37 const lastMaxItemsOnPage = React.useRef<number>(-1);
38 const layoutChanged = maxItemsOnPage !== lastMaxItemsOnPage.current;
39
40 const sortedTrackRefs =
41 typeof options.customSortFunction === 'function'
42 ? options.customSortFunction(trackReferences)
43 : sortTrackReferences(trackReferences);
44
45 let updatedTrackRefs: TrackReferenceOrPlaceholder[] = [...sortedTrackRefs];
46 if (layoutChanged === false) {
47 try {
48 updatedTrackRefs = updatePages(lastTrackRefs.current, sortedTrackRefs, maxItemsOnPage);
49 } catch (error) {
50 log.error('Error while running updatePages(): ', error);
51 }
52 }
53
54 // Save info for to compare against in the next update cycle.
55 if (layoutChanged) {
56 lastTrackRefs.current = sortedTrackRefs;
57 } else {
58 lastTrackRefs.current = updatedTrackRefs;
59 }
60 lastMaxItemsOnPage.current = maxItemsOnPage;
61
62 return updatedTrackRefs;
63}
Added view/app/src/vendor/livekit/react/hooks/useWarnAboutMissingStyles.ts view/app/src/vendor/livekit/react/hooks/useWarnAboutMissingStyles.ts +11 −0 View view/app/src/vendor/livekit/react/hooks/useWarnAboutMissingStyles.ts
@@ -0,0 +1,11 @@
1import * as React from 'react';
2import { warnAboutMissingStyles } from '../utils';
3
4/**
5 * @internal
6 */
7export function useWarnAboutMissingStyles() {
8 React.useEffect(() => {
9 warnAboutMissingStyles();
10 }, []);
11}
Added view/app/src/vendor/livekit/react/prefabs/Chat.tsx view/app/src/vendor/livekit/react/prefabs/Chat.tsx +154 −0 View view/app/src/vendor/livekit/react/prefabs/Chat.tsx
@@ -0,0 +1,154 @@
1import { type ChatMessage, type ChatOptions } from '@livekit/components-core';
2import * as React from 'react';
3import { useMaybeLayoutContext } from '../context';
4import { cloneSingleChild } from '../utils';
5import type { MessageFormatter } from '../components/ChatEntry';
6import { ChatEntry } from '../components/ChatEntry';
7import { useChat } from '../hooks/useChat';
8import { ChatToggle } from '../components';
9import ChatCloseIcon from '../assets/icons/ChatCloseIcon';
10import { lkT } from '@app/vendor/livekit/lk-i18n';
11
12/** @public */
13export interface ChatProps extends React.HTMLAttributes<HTMLDivElement>, ChatOptions {
14 messageFormatter?: MessageFormatter;
15}
16
17/**
18 * The Chat component provides ready-to-use chat functionality in a LiveKit room.
19 * Messages are distributed to all participants in the room in real-time.
20 *
21 * @remarks
22 * - Only users who are in the room at the time of dispatch will receive messages
23 * - Message history is not persisted between sessions
24 * - Requires `@livekit/components-styles` to be imported for styling
25 *
26 * @example
27 * ```tsx
28 * import '@livekit/components-styles';
29 *
30 * function Room() {
31 * return (
32 * <LiveKitRoom data-lk-theme="default">
33 * <Chat />
34 * </LiveKitRoom>
35 * );
36 * }
37 * ```
38 *
39 * For custom styling, refer to: https://docs.livekit.io/reference/components/react/concepts/style-components/
40 *
41 * @public
42 */
43export function Chat({
44 messageFormatter,
45 messageDecoder,
46 messageEncoder,
47 channelTopic,
48 ...props
49}: ChatProps) {
50 const ulRef = React.useRef<HTMLUListElement>(null);
51 const inputRef = React.useRef<HTMLInputElement>(null);
52
53 const chatOptions: ChatOptions = React.useMemo(() => {
54 return { messageDecoder, messageEncoder, channelTopic };
55 }, [messageDecoder, messageEncoder, channelTopic]);
56
57 const { chatMessages, send, isSending } = useChat(chatOptions);
58
59 const layoutContext = useMaybeLayoutContext();
60 const lastReadMsgAt = React.useRef<ChatMessage['timestamp']>(0);
61
62 async function handleSubmit(event: React.FormEvent) {
63 event.preventDefault();
64 if (inputRef.current && inputRef.current.value.trim() !== '') {
65 await send(inputRef.current.value);
66 inputRef.current.value = '';
67 inputRef.current.focus();
68 }
69 }
70
71 React.useEffect(() => {
72 if (ulRef) {
73 ulRef.current?.scrollTo({ top: ulRef.current.scrollHeight });
74 }
75 }, [ulRef, chatMessages]);
76
77 React.useEffect(() => {
78 if (!layoutContext || chatMessages.length === 0) {
79 return;
80 }
81
82 if (
83 layoutContext.widget.state?.showChat &&
84 chatMessages.length > 0 &&
85 lastReadMsgAt.current !== chatMessages[chatMessages.length - 1]?.timestamp
86 ) {
87 lastReadMsgAt.current = chatMessages[chatMessages.length - 1]?.timestamp;
88 return;
89 }
90
91 const unreadMessageCount = chatMessages.filter(
92 (msg) => !lastReadMsgAt.current || msg.timestamp > lastReadMsgAt.current,
93 ).length;
94
95 const { widget } = layoutContext;
96 if (unreadMessageCount > 0 && widget.state?.unreadMessages !== unreadMessageCount) {
97 widget.dispatch?.({ msg: 'unread_msg', count: unreadMessageCount });
98 }
99 }, [chatMessages, layoutContext?.widget]);
100
101 return (
102 <div {...props} className="lk-chat">
103 <div className="lk-chat-header">
104 Messages
105 {layoutContext && (
106 <ChatToggle className="lk-close-button">
107 <ChatCloseIcon />
108 </ChatToggle>
109 )}
110 </div>
111
112 <ul className="lk-list lk-chat-messages" ref={ulRef}>
113 {props.children
114 ? chatMessages.map((msg, idx) =>
115 cloneSingleChild(props.children, {
116 entry: msg,
117 key: msg.id ?? idx,
118 messageFormatter,
119 }),
120 )
121 : chatMessages.map((msg, idx, allMsg) => {
122 const hideName = idx >= 1 && allMsg[idx - 1].from === msg.from;
123 // If the time delta between two messages is bigger than 60s show timestamp.
124 const hideTimestamp = idx >= 1 && msg.timestamp - allMsg[idx - 1].timestamp < 60_000;
125
126 return (
127 <ChatEntry
128 key={msg.id ?? idx}
129 hideName={hideName}
130 hideTimestamp={hideName === false ? false : hideTimestamp} // If we show the name always show the timestamp as well.
131 entry={msg}
132 messageFormatter={messageFormatter}
133 />
134 );
135 })}
136 </ul>
137 <form className="lk-chat-form" onSubmit={handleSubmit}>
138 <input
139 className="lk-form-control lk-chat-form-input"
140 disabled={isSending}
141 ref={inputRef}
142 type="text"
143 placeholder={lkT("livekit.chatPlaceholder")}
144 onInput={(ev) => ev.stopPropagation()}
145 onKeyDown={(ev) => ev.stopPropagation()}
146 onKeyUp={(ev) => ev.stopPropagation()}
147 />
148 <button type="submit" className="lk-button lk-chat-form-button" disabled={isSending}>
149 Send
150 </button>
151 </form>
152 </div>
153 );
154}
Added view/app/src/vendor/livekit/react/prefabs/ControlBar.tsx view/app/src/vendor/livekit/react/prefabs/ControlBar.tsx +228 −0 View view/app/src/vendor/livekit/react/prefabs/ControlBar.tsx
@@ -0,0 +1,228 @@
1import { Track } from 'livekit-client';
2import * as React from 'react';
3import { MediaDeviceMenu } from './MediaDeviceMenu';
4import { DisconnectButton } from '../components/controls/DisconnectButton';
5import { TrackToggle } from '../components/controls/TrackToggle';
6import { ChatIcon, GearIcon, LeaveIcon } from '../assets/icons';
7import { ChatToggle } from '../components/controls/ChatToggle';
8import { useLocalParticipantPermissions, usePersistentUserChoices } from '../hooks';
9import { useMediaQuery } from '../hooks/internal';
10import { useMaybeLayoutContext } from '../context';
11import { supportsScreenSharing } from '@livekit/components-core';
12import { mergeProps } from '../utils';
13import { StartMediaButton } from '../components/controls/StartMediaButton';
14import { SettingsMenuToggle } from '../components/controls/SettingsMenuToggle';
15import { lkT } from '@app/vendor/livekit/lk-i18n';
16
17/** @public */
18export type ControlBarControls = {
19 microphone?: boolean;
20 camera?: boolean;
21 chat?: boolean;
22 screenShare?: boolean;
23 leave?: boolean;
24 settings?: boolean;
25};
26
27const trackSourceToProtocol = (source: Track.Source) => {
28 // NOTE: this mapping avoids importing the protocol package as that leads to a significant bundle size increase
29 switch (source) {
30 case Track.Source.Camera:
31 return 1;
32 case Track.Source.Microphone:
33 return 2;
34 case Track.Source.ScreenShare:
35 return 3;
36 default:
37 return 0;
38 }
39};
40
41/** @public */
42export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
43 onDeviceError?: (error: { source: Track.Source; error: Error }) => void;
44 variation?: 'minimal' | 'verbose' | 'textOnly';
45 controls?: ControlBarControls;
46 /**
47 * If `true`, the user's device choices will be persisted.
48 * This will enable the user to have the same device choices when they rejoin the room.
49 * @defaultValue true
50 * @alpha
51 */
52 saveUserChoices?: boolean;
53}
54
55/**
56 * The `ControlBar` prefab gives the user the basic user interface to control their
57 * media devices (camera, microphone and screen share), open the `Chat` and leave the room.
58 *
59 * @remarks
60 * This component is built with other LiveKit components like `TrackToggle`,
61 * `DeviceSelectorButton`, `DisconnectButton` and `StartAudio`.
62 *
63 * @example
64 * ```tsx
65 * <LiveKitRoom>
66 * <ControlBar />
67 * </LiveKitRoom>
68 * ```
69 * @public
70 */
71export function ControlBar({
72 variation,
73 controls,
74 saveUserChoices = true,
75 onDeviceError,
76 ...props
77}: ControlBarProps) {
78 const [isChatOpen, setIsChatOpen] = React.useState(false);
79 const layoutContext = useMaybeLayoutContext();
80 React.useEffect(() => {
81 if (layoutContext?.widget.state?.showChat !== undefined) {
82 setIsChatOpen(layoutContext?.widget.state?.showChat);
83 }
84 }, [layoutContext?.widget.state?.showChat]);
85 const isTooLittleSpace = useMediaQuery(`(max-width: ${isChatOpen ? 1000 : 760}px)`);
86
87 const defaultVariation = isTooLittleSpace ? 'minimal' : 'verbose';
88 variation ??= defaultVariation;
89
90 const visibleControls = { leave: true, ...controls };
91
92 const localPermissions = useLocalParticipantPermissions();
93
94 if (!localPermissions) {
95 visibleControls.camera = false;
96 visibleControls.chat = false;
97 visibleControls.microphone = false;
98 visibleControls.screenShare = false;
99 } else {
100 const canPublishSource = (source: Track.Source) => {
101 return (
102 localPermissions.canPublish &&
103 (localPermissions.canPublishSources.length === 0 ||
104 localPermissions.canPublishSources.includes(trackSourceToProtocol(source)))
105 );
106 };
107 visibleControls.camera ??= canPublishSource(Track.Source.Camera);
108 visibleControls.microphone ??= canPublishSource(Track.Source.Microphone);
109 visibleControls.screenShare ??= canPublishSource(Track.Source.ScreenShare);
110 visibleControls.chat ??= localPermissions.canPublishData && controls?.chat;
111 }
112
113 const showIcon = React.useMemo(
114 () => variation === 'minimal' || variation === 'verbose',
115 [variation],
116 );
117 const showText = React.useMemo(
118 () => variation === 'textOnly' || variation === 'verbose',
119 [variation],
120 );
121
122 const browserSupportsScreenSharing = supportsScreenSharing();
123
124 const [isScreenShareEnabled, setIsScreenShareEnabled] = React.useState(false);
125
126 const onScreenShareChange = React.useCallback(
127 (enabled: boolean) => {
128 setIsScreenShareEnabled(enabled);
129 },
130 [setIsScreenShareEnabled],
131 );
132
133 const htmlProps = mergeProps({ className: 'lk-control-bar' }, props);
134
135 const {
136 saveAudioInputEnabled,
137 saveVideoInputEnabled,
138 saveAudioInputDeviceId,
139 saveVideoInputDeviceId,
140 } = usePersistentUserChoices({ preventSave: !saveUserChoices });
141
142 const microphoneOnChange = React.useCallback(
143 (enabled: boolean, isUserInitiated: boolean) =>
144 isUserInitiated ? saveAudioInputEnabled(enabled) : null,
145 [saveAudioInputEnabled],
146 );
147
148 const cameraOnChange = React.useCallback(
149 (enabled: boolean, isUserInitiated: boolean) =>
150 isUserInitiated ? saveVideoInputEnabled(enabled) : null,
151 [saveVideoInputEnabled],
152 );
153
154 return (
155 <div {...htmlProps}>
156 {visibleControls.microphone && (
157 <div className="lk-button-group">
158 <TrackToggle
159 source={Track.Source.Microphone}
160 showIcon={showIcon}
161 onChange={microphoneOnChange}
162 onDeviceError={(error) => onDeviceError?.({ source: Track.Source.Microphone, error })}
163 >
164 {showText && lkT("livekit.microphone")}
165 </TrackToggle>
166 <div className="lk-button-group-menu">
167 <MediaDeviceMenu
168 kind="audioinput"
169 onActiveDeviceChange={(_kind, deviceId) =>
170 saveAudioInputDeviceId(deviceId ?? 'default')
171 }
172 />
173 </div>
174 </div>
175 )}
176 {visibleControls.camera && (
177 <div className="lk-button-group">
178 <TrackToggle
179 source={Track.Source.Camera}
180 showIcon={showIcon}
181 onChange={cameraOnChange}
182 onDeviceError={(error) => onDeviceError?.({ source: Track.Source.Camera, error })}
183 >
184 {showText && lkT("livekit.camera")}
185 </TrackToggle>
186 <div className="lk-button-group-menu">
187 <MediaDeviceMenu
188 kind="videoinput"
189 onActiveDeviceChange={(_kind, deviceId) =>
190 saveVideoInputDeviceId(deviceId ?? 'default')
191 }
192 />
193 </div>
194 </div>
195 )}
196 {visibleControls.screenShare && browserSupportsScreenSharing && (
197 <TrackToggle
198 source={Track.Source.ScreenShare}
199 captureOptions={{ audio: true, selfBrowserSurface: 'include' }}
200 showIcon={showIcon}
201 onChange={onScreenShareChange}
202 onDeviceError={(error) => onDeviceError?.({ source: Track.Source.ScreenShare, error })}
203 >
204 {showText && lkT(isScreenShareEnabled ? "livekit.stopScreenShare" : "livekit.shareScreen")}
205 </TrackToggle>
206 )}
207 {visibleControls.chat && (
208 <ChatToggle>
209 {showIcon && <ChatIcon />}
210 {showText && lkT("livekit.chat")}
211 </ChatToggle>
212 )}
213 {visibleControls.settings && (
214 <SettingsMenuToggle>
215 {showIcon && <GearIcon />}
216 {showText && lkT("livekit.settings")}
217 </SettingsMenuToggle>
218 )}
219 {visibleControls.leave && (
220 <DisconnectButton>
221 {showIcon && <LeaveIcon />}
222 {showText && lkT("livekit.leave")}
223 </DisconnectButton>
224 )}
225 <StartMediaButton />
226 </div>
227 );
228}
Added view/app/src/vendor/livekit/react/prefabs/index.ts view/app/src/vendor/livekit/react/prefabs/index.ts +5 −0 View view/app/src/vendor/livekit/react/prefabs/index.ts
@@ -0,0 +1,5 @@
1export { Chat, type ChatProps } from './Chat';
2export { PreJoin, type PreJoinProps, usePreviewDevice, usePreviewTracks } from './PreJoin';
3export { VideoConference, type VideoConferenceProps } from './VideoConference';
4export { ControlBar, type ControlBarProps, type ControlBarControls } from './ControlBar';
5export { MediaDeviceMenu, type MediaDeviceMenuProps } from './MediaDeviceMenu';
Added view/app/src/vendor/livekit/react/prefabs/MediaDeviceMenu.tsx view/app/src/vendor/livekit/react/prefabs/MediaDeviceMenu.tsx +160 −0 View view/app/src/vendor/livekit/react/prefabs/MediaDeviceMenu.tsx
@@ -0,0 +1,160 @@
1import { computeMenuPosition, wasClickOutside, log } from '@livekit/components-core';
2import * as React from 'react';
3import { MediaDeviceSelect } from '../components/controls/MediaDeviceSelect';
4import type { LocalAudioTrack, LocalVideoTrack } from 'livekit-client';
5import { lkT } from '@app/vendor/livekit/lk-i18n';
6
7/** @public */
8export interface MediaDeviceMenuProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
9 kind?: MediaDeviceKind;
10 initialSelection?: string;
11 onActiveDeviceChange?: (kind: MediaDeviceKind, deviceId: string) => void;
12 tracks?: Partial<Record<MediaDeviceKind, LocalAudioTrack | LocalVideoTrack | undefined>>;
13 /**
14 * this will call getUserMedia if the permissions are not yet given to enumerate the devices with device labels.
15 * in some browsers multiple calls to getUserMedia result in multiple permission prompts.
16 * It's generally advised only flip this to true, once a (preview) track has been acquired successfully with the
17 * appropriate permissions.
18 *
19 * @see {@link PreJoin}
20 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices | MDN enumerateDevices}
21 */
22 requestPermissions?: boolean;
23}
24
25/**
26 * The `MediaDeviceMenu` component is a button that opens a menu that lists
27 * all media devices and allows the user to select them.
28 *
29 * @remarks
30 * This component is implemented with the `MediaDeviceSelect` LiveKit components.
31 *
32 * @example
33 * ```tsx
34 * <LiveKitRoom>
35 * <MediaDeviceMenu />
36 * </LiveKitRoom>
37 * ```
38 * @public
39 */
40export function MediaDeviceMenu({
41 kind,
42 initialSelection,
43 onActiveDeviceChange,
44 tracks,
45 requestPermissions = false,
46 ...props
47}: MediaDeviceMenuProps) {
48 const [isOpen, setIsOpen] = React.useState(false);
49 const [devices, setDevices] = React.useState<MediaDeviceInfo[]>([]);
50 const [updateRequired, setUpdateRequired] = React.useState<boolean>(true);
51 const [needPermissions, setNeedPermissions] = React.useState(requestPermissions);
52
53 const handleActiveDeviceChange = (kind: MediaDeviceKind, deviceId: string) => {
54 log.debug('handle device change');
55 setIsOpen(false);
56 onActiveDeviceChange?.(kind, deviceId);
57 };
58
59 const button = React.useRef<HTMLButtonElement>(null);
60 const tooltip = React.useRef<HTMLDivElement>(null);
61
62 React.useLayoutEffect(() => {
63 if (isOpen) {
64 setNeedPermissions(true);
65 }
66 }, [isOpen]);
67
68 React.useLayoutEffect(() => {
69 let cleanup: ReturnType<typeof computeMenuPosition> | undefined;
70 if (button.current && tooltip.current && (devices || updateRequired)) {
71 cleanup = computeMenuPosition(button.current, tooltip.current, (x, y) => {
72 if (tooltip.current) {
73 Object.assign(tooltip.current.style, { left: `${x}px`, top: `${y}px` });
74 }
75 });
76 }
77 setUpdateRequired(false);
78 return () => {
79 cleanup?.();
80 };
81 }, [button, tooltip, devices, updateRequired]);
82
83 const handleClickOutside = React.useCallback(
84 (event: MouseEvent) => {
85 if (!tooltip.current) {
86 return;
87 }
88 if (event.target === button.current) {
89 return;
90 }
91 if (isOpen && wasClickOutside(tooltip.current, event)) {
92 setIsOpen(false);
93 }
94 },
95 [isOpen, tooltip, button],
96 );
97
98 React.useEffect(() => {
99 document.addEventListener<'click'>('click', handleClickOutside);
100 return () => {
101 document.removeEventListener<'click'>('click', handleClickOutside);
102 };
103 }, [handleClickOutside]);
104
105 return (
106 <>
107 <button
108 className="lk-button lk-button-menu"
109 aria-pressed={isOpen}
110 {...props}
111 onClick={() => setIsOpen(!isOpen)}
112 ref={button}
113 >
114 {props.children}
115 </button>
116 {/** only render when enabled in order to make sure that the permissions are requested only if the menu is enabled */}
117 {!props.disabled && (
118 <div
119 className="lk-device-menu"
120 ref={tooltip}
121 style={{ visibility: isOpen ? 'visible' : 'hidden' }}
122 >
123 {kind ? (
124 <MediaDeviceSelect
125 initialSelection={initialSelection}
126 onActiveDeviceChange={(deviceId) => handleActiveDeviceChange(kind, deviceId)}
127 onDeviceListChange={setDevices}
128 kind={kind}
129 track={tracks?.[kind]}
130 requestPermissions={needPermissions}
131 />
132 ) : (
133 <>
134 <div className="lk-device-menu-heading">{lkT("livekit.audioInputs")}</div>
135 <MediaDeviceSelect
136 kind="audioinput"
137 onActiveDeviceChange={(deviceId) =>
138 handleActiveDeviceChange('audioinput', deviceId)
139 }
140 onDeviceListChange={setDevices}
141 track={tracks?.audioinput}
142 requestPermissions={needPermissions}
143 />
144 <div className="lk-device-menu-heading">{lkT("livekit.videoInputs")}</div>
145 <MediaDeviceSelect
146 kind="videoinput"
147 onActiveDeviceChange={(deviceId) =>
148 handleActiveDeviceChange('videoinput', deviceId)
149 }
150 onDeviceListChange={setDevices}
151 track={tracks?.videoinput}
152 requestPermissions={needPermissions}
153 />
154 </>
155 )}
156 </div>
157 )}
158 </>
159 );
160}
Added view/app/src/vendor/livekit/react/prefabs/PreJoin.tsx view/app/src/vendor/livekit/react/prefabs/PreJoin.tsx +441 −0 View view/app/src/vendor/livekit/react/prefabs/PreJoin.tsx
@@ -0,0 +1,441 @@
1import type {
2 CreateLocalTracksOptions,
3 LocalAudioTrack,
4 LocalVideoTrack,
5 TrackProcessor,
6} from 'livekit-client';
7import {
8 createLocalAudioTrack,
9 createLocalTracks,
10 createLocalVideoTrack,
11 facingModeFromLocalTrack,
12 Track,
13 VideoPresets,
14 Mutex,
15} from 'livekit-client';
16import * as React from 'react';
17import { MediaDeviceMenu } from './MediaDeviceMenu';
18import { TrackToggle } from '../components/controls/TrackToggle';
19import type { LocalUserChoices } from '@livekit/components-core';
20import { log } from '@livekit/components-core';
21import { ParticipantPlaceholder } from '../assets/images';
22import { useMediaDevices, usePersistentUserChoices } from '../hooks';
23import { useWarnAboutMissingStyles } from '../hooks/useWarnAboutMissingStyles';
24import { roomOptionsStringifyReplacer } from '../utils';
25
26/**
27 * Props for the PreJoin component.
28 * @public
29 */
30export interface PreJoinProps extends Omit<
31 React.HTMLAttributes<HTMLDivElement>,
32 'onSubmit' | 'onError'
33> {
34 /** This function is called with the `LocalUserChoices` if validation is passed. */
35 onSubmit?: (values: LocalUserChoices) => void;
36 /**
37 * Provide your custom validation function. Only if validation is successful the user choices are passed to the onSubmit callback.
38 */
39 onValidate?: (values: LocalUserChoices) => boolean;
40 onError?: (error: Error) => void;
41 /** Prefill the input form with initial values. */
42 defaults?: Partial<LocalUserChoices>;
43 /** Display a debug window for your convenience. */
44 debug?: boolean;
45 joinLabel?: string;
46 micLabel?: string;
47 camLabel?: string;
48 userLabel?: string;
49 /**
50 * If true, user choices are persisted across sessions.
51 * @defaultValue true
52 * @alpha
53 */
54 persistUserChoices?: boolean;
55 videoProcessor?: TrackProcessor<Track.Kind.Video>;
56}
57
58/** @public */
59export function usePreviewTracks(
60 options: CreateLocalTracksOptions,
61 onError?: (err: Error) => void,
62) {
63 const [tracks, setTracks] = React.useState<Array<LocalAudioTrack | LocalVideoTrack>>();
64
65 const trackLock = React.useMemo(() => new Mutex(), []);
66
67 React.useEffect(() => {
68 let needsCleanup = false;
69 let localTracks: Array<LocalAudioTrack | LocalVideoTrack> = [];
70 trackLock.lock().then(async (unlock) => {
71 try {
72 if (options.audio || options.video) {
73 localTracks = (await createLocalTracks(options)) as Array<
74 LocalAudioTrack | LocalVideoTrack
75 >;
76
77 if (needsCleanup) {
78 localTracks.forEach((tr) => tr.stop());
79 } else {
80 setTracks(localTracks);
81 }
82 }
83 } catch (e: unknown) {
84 if (onError && e instanceof Error) {
85 onError(e);
86 } else {
87 log.error(e);
88 }
89 } finally {
90 unlock();
91 }
92 });
93
94 return () => {
95 needsCleanup = true;
96 localTracks.forEach((track) => {
97 track.stop();
98 });
99 };
100 }, [JSON.stringify(options, roomOptionsStringifyReplacer), onError, trackLock]);
101
102 return tracks;
103}
104
105/**
106 * @public
107 * @deprecated use `usePreviewTracks` instead
108 */
109export function usePreviewDevice<T extends LocalVideoTrack | LocalAudioTrack>(
110 enabled: boolean,
111 deviceId: string,
112 kind: 'videoinput' | 'audioinput',
113) {
114 const [deviceError, setDeviceError] = React.useState<Error | null>(null);
115 const [isCreatingTrack, setIsCreatingTrack] = React.useState<boolean>(false);
116
117 const devices = useMediaDevices({ kind });
118 const [selectedDevice, setSelectedDevice] = React.useState<MediaDeviceInfo | undefined>(
119 undefined,
120 );
121
122 const [localTrack, setLocalTrack] = React.useState<T>();
123 const [localDeviceId, setLocalDeviceId] = React.useState<string>(deviceId);
124
125 React.useEffect(() => {
126 setLocalDeviceId(deviceId);
127 }, [deviceId]);
128
129 const createTrack = async (deviceId: string, kind: 'videoinput' | 'audioinput') => {
130 try {
131 const track =
132 kind === 'videoinput'
133 ? await createLocalVideoTrack({
134 deviceId,
135 resolution: VideoPresets.h720.resolution,
136 })
137 : await createLocalAudioTrack({ deviceId });
138
139 const newDeviceId = await track.getDeviceId(false);
140 if (newDeviceId && deviceId !== newDeviceId) {
141 prevDeviceId.current = newDeviceId;
142 setLocalDeviceId(newDeviceId);
143 }
144 setLocalTrack(track as T);
145 } catch (e) {
146 if (e instanceof Error) {
147 setDeviceError(e);
148 }
149 }
150 };
151
152 const switchDevice = async (track: LocalVideoTrack | LocalAudioTrack, id: string) => {
153 await track.setDeviceId(id);
154 prevDeviceId.current = id;
155 };
156
157 const prevDeviceId = React.useRef(localDeviceId);
158
159 React.useEffect(() => {
160 if (enabled && !localTrack && !deviceError && !isCreatingTrack) {
161 log.debug('creating track', kind);
162 setIsCreatingTrack(true);
163 createTrack(localDeviceId, kind).finally(() => {
164 setIsCreatingTrack(false);
165 });
166 }
167 }, [enabled, localTrack, deviceError, isCreatingTrack]);
168
169 // switch camera device
170 React.useEffect(() => {
171 if (!localTrack) {
172 return;
173 }
174 if (!enabled) {
175 log.debug(`muting ${kind} track`);
176 localTrack.mute().then(() => log.debug(localTrack.mediaStreamTrack));
177 } else if (selectedDevice?.deviceId && prevDeviceId.current !== selectedDevice?.deviceId) {
178 log.debug(`switching ${kind} device from`, prevDeviceId.current, selectedDevice.deviceId);
179 switchDevice(localTrack, selectedDevice.deviceId);
180 } else {
181 log.debug(`unmuting local ${kind} track`);
182 localTrack.unmute();
183 }
184 }, [localTrack, selectedDevice, enabled, kind]);
185
186 React.useEffect(() => {
187 return () => {
188 if (localTrack) {
189 log.debug(`stopping local ${kind} track`);
190 localTrack.stop();
191 localTrack.mute();
192 }
193 };
194 }, []);
195
196 React.useEffect(() => {
197 setSelectedDevice(devices?.find((dev) => dev.deviceId === localDeviceId));
198 }, [localDeviceId, devices]);
199
200 return {
201 selectedDevice,
202 localTrack,
203 deviceError,
204 };
205}
206
207/**
208 * The `PreJoin` prefab component is normally presented to the user before he enters a room.
209 * This component allows the user to check and select the preferred media device (camera and microphone).
210 * On submit the user decisions are returned, which can then be passed on to the `LiveKitRoom` so that the user enters the room with the correct media devices.
211 *
212 * @remarks
213 * This component is independent of the `LiveKitRoom` component and should not be nested within it.
214 * Because it only accesses the local media tracks this component is self-contained and works without connection to the LiveKit server.
215 *
216 * @example
217 * ```tsx
218 * <PreJoin />
219 * ```
220 * @public
221 */
222export function PreJoin({
223 defaults = {},
224 onValidate,
225 onSubmit,
226 onError,
227 debug,
228 joinLabel = 'Join Room',
229 micLabel = 'Microphone',
230 camLabel = 'Camera',
231 userLabel = 'Username',
232 persistUserChoices = true,
233 videoProcessor,
234 ...htmlProps
235}: PreJoinProps) {
236 const {
237 userChoices: initialUserChoices,
238 saveAudioInputDeviceId,
239 saveAudioInputEnabled,
240 saveVideoInputDeviceId,
241 saveVideoInputEnabled,
242 saveUsername,
243 } = usePersistentUserChoices({
244 defaults,
245 preventSave: !persistUserChoices,
246 preventLoad: !persistUserChoices,
247 });
248
249 const [userChoices, setUserChoices] = React.useState(initialUserChoices);
250
251 // Initialize device settings
252 const [audioEnabled, setAudioEnabled] = React.useState<boolean>(userChoices.audioEnabled);
253 const [videoEnabled, setVideoEnabled] = React.useState<boolean>(userChoices.videoEnabled);
254 const [audioDeviceId, setAudioDeviceId] = React.useState<string>(userChoices.audioDeviceId);
255 const [videoDeviceId, setVideoDeviceId] = React.useState<string>(userChoices.videoDeviceId);
256 const [username, setUsername] = React.useState(userChoices.username);
257
258 // Save user choices to persistent storage.
259 React.useEffect(() => {
260 saveAudioInputEnabled(audioEnabled);
261 }, [audioEnabled, saveAudioInputEnabled]);
262 React.useEffect(() => {
263 saveVideoInputEnabled(videoEnabled);
264 }, [videoEnabled, saveVideoInputEnabled]);
265 React.useEffect(() => {
266 saveAudioInputDeviceId(audioDeviceId);
267 }, [audioDeviceId, saveAudioInputDeviceId]);
268 React.useEffect(() => {
269 saveVideoInputDeviceId(videoDeviceId);
270 }, [videoDeviceId, saveVideoInputDeviceId]);
271 React.useEffect(() => {
272 saveUsername(username);
273 }, [username, saveUsername]);
274
275 const tracks = usePreviewTracks(
276 {
277 audio: audioEnabled ? { deviceId: initialUserChoices.audioDeviceId } : false,
278 video: videoEnabled
279 ? { deviceId: initialUserChoices.videoDeviceId, processor: videoProcessor }
280 : false,
281 },
282 onError,
283 );
284
285 const videoEl = React.useRef(null);
286
287 const videoTrack = React.useMemo(
288 () => tracks?.filter((track) => track.kind === Track.Kind.Video)[0] as LocalVideoTrack,
289 [tracks],
290 );
291
292 const facingMode = React.useMemo(() => {
293 if (videoTrack) {
294 const { facingMode } = facingModeFromLocalTrack(videoTrack);
295 return facingMode;
296 } else {
297 return 'undefined';
298 }
299 }, [videoTrack]);
300
301 const audioTrack = React.useMemo(
302 () => tracks?.filter((track) => track.kind === Track.Kind.Audio)[0] as LocalAudioTrack,
303 [tracks],
304 );
305
306 React.useEffect(() => {
307 if (videoEl.current && videoTrack) {
308 videoTrack.unmute();
309 videoTrack.attach(videoEl.current);
310 }
311
312 return () => {
313 videoTrack?.detach();
314 };
315 }, [videoTrack]);
316
317 const [isValid, setIsValid] = React.useState<boolean>();
318
319 const handleValidation = React.useCallback(
320 (values: LocalUserChoices) => {
321 if (typeof onValidate === 'function') {
322 return onValidate(values);
323 } else {
324 return values.username !== '';
325 }
326 },
327 [onValidate],
328 );
329
330 React.useEffect(() => {
331 const newUserChoices = {
332 username,
333 videoEnabled,
334 videoDeviceId,
335 audioEnabled,
336 audioDeviceId,
337 };
338 setUserChoices(newUserChoices);
339 setIsValid(handleValidation(newUserChoices));
340 }, [username, videoEnabled, handleValidation, audioEnabled, audioDeviceId, videoDeviceId]);
341
342 function handleSubmit(event: React.FormEvent) {
343 event.preventDefault();
344 if (handleValidation(userChoices)) {
345 if (typeof onSubmit === 'function') {
346 onSubmit(userChoices);
347 }
348 } else {
349 log.warn('Validation failed with: ', userChoices);
350 }
351 }
352
353 useWarnAboutMissingStyles();
354
355 return (
356 <div className="lk-prejoin" {...htmlProps}>
357 <div className="lk-video-container">
358 {videoTrack && (
359 <video ref={videoEl} width="1280" height="720" data-lk-facing-mode={facingMode} />
360 )}
361 {(!videoTrack || !videoEnabled) && (
362 <div className="lk-camera-off-note">
363 <ParticipantPlaceholder />
364 </div>
365 )}
366 </div>
367 <div className="lk-button-group-container">
368 <div className="lk-button-group audio">
369 <TrackToggle
370 initialState={audioEnabled}
371 source={Track.Source.Microphone}
372 onChange={(enabled) => setAudioEnabled(enabled)}
373 >
374 {micLabel}
375 </TrackToggle>
376 <div className="lk-button-group-menu">
377 <MediaDeviceMenu
378 initialSelection={audioDeviceId}
379 kind="audioinput"
380 disabled={!audioTrack}
381 tracks={{ audioinput: audioTrack }}
382 onActiveDeviceChange={(_, id) => setAudioDeviceId(id)}
383 />
384 </div>
385 </div>
386 <div className="lk-button-group video">
387 <TrackToggle
388 initialState={videoEnabled}
389 source={Track.Source.Camera}
390 onChange={(enabled) => setVideoEnabled(enabled)}
391 >
392 {camLabel}
393 </TrackToggle>
394 <div className="lk-button-group-menu">
395 <MediaDeviceMenu
396 initialSelection={videoDeviceId}
397 kind="videoinput"
398 disabled={!videoTrack}
399 tracks={{ videoinput: videoTrack }}
400 onActiveDeviceChange={(_, id) => setVideoDeviceId(id)}
401 />
402 </div>
403 </div>
404 </div>
405
406 <form className="lk-username-container">
407 <input
408 className="lk-form-control"
409 id="username"
410 name="username"
411 type="text"
412 defaultValue={username}
413 placeholder={userLabel}
414 onChange={(inputEl) => setUsername(inputEl.target.value)}
415 autoComplete="off"
416 />
417 <button
418 className="lk-button lk-join-button"
419 type="submit"
420 onClick={handleSubmit}
421 disabled={!isValid}
422 >
423 {joinLabel}
424 </button>
425 </form>
426
427 {debug && (
428 <>
429 <strong>User Choices:</strong>
430 <ul className="lk-list" style={{ overflow: 'hidden', maxWidth: '15rem' }}>
431 <li>Username: {`${userChoices.username}`}</li>
432 <li>Video Enabled: {`${userChoices.videoEnabled}`}</li>
433 <li>Audio Enabled: {`${userChoices.audioEnabled}`}</li>
434 <li>Video Device: {`${userChoices.videoDeviceId}`}</li>
435 <li>Audio Device: {`${userChoices.audioDeviceId}`}</li>
436 </ul>
437 </>
438 )}
439 </div>
440 );
441}
Added view/app/src/vendor/livekit/react/prefabs/VideoConference.tsx view/app/src/vendor/livekit/react/prefabs/VideoConference.tsx +180 −0 View view/app/src/vendor/livekit/react/prefabs/VideoConference.tsx
@@ -0,0 +1,180 @@
1import type {
2 MessageDecoder,
3 MessageEncoder,
4 TrackReferenceOrPlaceholder,
5 WidgetState,
6} from '@livekit/components-core';
7import { isEqualTrackRef, isTrackReference, isWeb, log } from '@livekit/components-core';
8import { RoomEvent, Track } from 'livekit-client';
9import * as React from 'react';
10import type { MessageFormatter } from '../components';
11import {
12 CarouselLayout,
13 ConnectionStateToast,
14 FocusLayout,
15 FocusLayoutContainer,
16 GridLayout,
17 LayoutContextProvider,
18 ParticipantTile,
19 RoomAudioRenderer,
20} from '../components';
21import { useCreateLayoutContext } from '../context';
22import { usePinnedTracks, useTracks } from '../hooks';
23import { Chat } from './Chat';
24import { ControlBar } from './ControlBar';
25import { useWarnAboutMissingStyles } from '../hooks/useWarnAboutMissingStyles';
26
27/**
28 * @public
29 */
30export interface VideoConferenceProps extends React.HTMLAttributes<HTMLDivElement> {
31 chatMessageFormatter?: MessageFormatter;
32 chatMessageEncoder?: MessageEncoder;
33 chatMessageDecoder?: MessageDecoder;
34 /** @alpha */
35 SettingsComponent?: React.ComponentType;
36}
37
38/**
39 * The `VideoConference` ready-made component is your drop-in solution for a classic video conferencing application.
40 * It provides functionality such as focusing on one participant, grid view with pagination to handle large numbers
41 * of participants, basic non-persistent chat, screen sharing, and more.
42 *
43 * @remarks
44 * The component is implemented with other LiveKit components like `FocusContextProvider`,
45 * `GridLayout`, `ControlBar`, `FocusLayoutContainer` and `FocusLayout`.
46 * You can use these components as a starting point for your own custom video conferencing application.
47 *
48 * @example
49 * ```tsx
50 * <LiveKitRoom>
51 * <VideoConference />
52 * <LiveKitRoom>
53 * ```
54 * @public
55 */
56export function VideoConference({
57 chatMessageFormatter,
58 chatMessageDecoder,
59 chatMessageEncoder,
60 SettingsComponent,
61 ...props
62}: VideoConferenceProps) {
63 const [widgetState, setWidgetState] = React.useState<WidgetState>({
64 showChat: false,
65 unreadMessages: 0,
66 showSettings: false,
67 });
68 const lastAutoFocusedScreenShareTrack = React.useRef<TrackReferenceOrPlaceholder | null>(null);
69
70 const tracks = useTracks(
71 [
72 { source: Track.Source.Camera, withPlaceholder: true },
73 { source: Track.Source.ScreenShare, withPlaceholder: false },
74 ],
75 { updateOnlyOn: [RoomEvent.ActiveSpeakersChanged], onlySubscribed: false },
76 );
77
78 const widgetUpdate = (state: WidgetState) => {
79 log.debug('updating widget state', state);
80 setWidgetState(state);
81 };
82
83 const layoutContext = useCreateLayoutContext();
84
85 const screenShareTracks = tracks
86 .filter(isTrackReference)
87 .filter((track) => track.publication.source === Track.Source.ScreenShare);
88
89 const focusTrack = usePinnedTracks(layoutContext)?.[0];
90 const carouselTracks = tracks.filter((track) => !isEqualTrackRef(track, focusTrack));
91
92 React.useEffect(() => {
93 // If screen share tracks are published, and no pin is set explicitly, auto set the screen share.
94 if (
95 screenShareTracks.some((track) => track.publication.isSubscribed) &&
96 lastAutoFocusedScreenShareTrack.current === null
97 ) {
98 log.debug('Auto set screen share focus:', { newScreenShareTrack: screenShareTracks[0] });
99 layoutContext.pin.dispatch?.({ msg: 'set_pin', trackReference: screenShareTracks[0] });
100 lastAutoFocusedScreenShareTrack.current = screenShareTracks[0];
101 } else if (
102 lastAutoFocusedScreenShareTrack.current &&
103 !screenShareTracks.some(
104 (track) =>
105 track.publication.trackSid ===
106 lastAutoFocusedScreenShareTrack.current?.publication?.trackSid,
107 )
108 ) {
109 log.debug('Auto clearing screen share focus.');
110 layoutContext.pin.dispatch?.({ msg: 'clear_pin' });
111 lastAutoFocusedScreenShareTrack.current = null;
112 }
113 if (focusTrack && !isTrackReference(focusTrack)) {
114 const updatedFocusTrack = tracks.find(
115 (tr) =>
116 tr.participant.identity === focusTrack.participant.identity &&
117 tr.source === focusTrack.source,
118 );
119 if (updatedFocusTrack !== focusTrack && isTrackReference(updatedFocusTrack)) {
120 layoutContext.pin.dispatch?.({ msg: 'set_pin', trackReference: updatedFocusTrack });
121 }
122 }
123 }, [
124 screenShareTracks
125 .map((ref) => `${ref.publication.trackSid}_${ref.publication.isSubscribed}`)
126 .join(),
127 focusTrack?.publication?.trackSid,
128 tracks,
129 ]);
130
131 useWarnAboutMissingStyles();
132
133 return (
134 <div className="lk-video-conference" {...props}>
135 {isWeb() && (
136 <LayoutContextProvider
137 value={layoutContext}
138 // onPinChange={handleFocusStateChange}
139 onWidgetChange={widgetUpdate}
140 >
141 <div className="lk-video-conference-inner">
142 {!focusTrack ? (
143 <div className="lk-grid-layout-wrapper">
144 <GridLayout tracks={tracks}>
145 <ParticipantTile />
146 </GridLayout>
147 </div>
148 ) : (
149 <div className="lk-focus-layout-wrapper">
150 <FocusLayoutContainer>
151 <CarouselLayout tracks={carouselTracks}>
152 <ParticipantTile />
153 </CarouselLayout>
154 {focusTrack && <FocusLayout trackRef={focusTrack} />}
155 </FocusLayoutContainer>
156 </div>
157 )}
158 <ControlBar controls={{ chat: true, settings: !!SettingsComponent }} />
159 </div>
160 <Chat
161 style={{ display: widgetState.showChat ? 'grid' : 'none' }}
162 messageFormatter={chatMessageFormatter}
163 messageEncoder={chatMessageEncoder}
164 messageDecoder={chatMessageDecoder}
165 />
166 {SettingsComponent && (
167 <div
168 className="lk-settings-menu-modal"
169 style={{ display: widgetState.showSettings ? 'block' : 'none' }}
170 >
171 <SettingsComponent />
172 </div>
173 )}
174 </LayoutContextProvider>
175 )}
176 <RoomAudioRenderer />
177 <ConnectionStateToast />
178 </div>
179 );
180}
Added view/app/src/vendor/livekit/react/types/events.d.ts view/app/src/vendor/livekit/react/types/events.d.ts +10 −0 View view/app/src/vendor/livekit/react/types/events.d.ts
@@ -0,0 +1,10 @@
1declare module 'events' {
2 export class EventEmitter {
3 on(event: string | symbol, listener: (...args: any[]) => void): this;
4 once(event: string | symbol, listener: (...args: any[]) => void): this;
5 off(event: string | symbol, listener: (...args: any[]) => void): this;
6 emit(event: string | symbol, ...args: any[]): boolean;
7 removeListener(event: string | symbol, listener: (...args: any[]) => void): this;
8 removeAllListeners(event?: string | symbol): this;
9 }
10}
Added view/app/src/vendor/livekit/react/index.ts view/app/src/vendor/livekit/react/index.ts +30 −0 View view/app/src/vendor/livekit/react/index.ts
@@ -0,0 +1,30 @@
1export * from './components';
2
3export * from './hooks';
4
5export * from './prefabs';
6
7export * from './context';
8
9export * from './assets/icons';
10
11export * from './assets/images';
12
13// Re-exports from core
14export { setLogLevel, setLogExtension, isTrackReference } from '@livekit/components-core';
15export type {
16 ChatMessage,
17 ReceivedMessage,
18 ReceivedChatMessage,
19 ReceivedUserTranscriptionMessage,
20 MessageDecoder,
21 MessageEncoder,
22 LocalUserChoices,
23 TrackReference,
24 TrackReferenceOrPlaceholder,
25 ParticipantClickEvent,
26 ParticipantIdentifier,
27 PinState,
28 WidgetState,
29 GridLayoutDefinition,
30} from '@livekit/components-core';
Added view/app/src/vendor/livekit/react/mergeProps.ts view/app/src/vendor/livekit/react/mergeProps.ts +87 −0 View view/app/src/vendor/livekit/react/mergeProps.ts
@@ -0,0 +1,87 @@
1/*
2 * Copyright 2020 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12
13import clsx from 'clsx';
14
15/**
16 * Calls all functions in the order they were chained with the same arguments.
17 * @internal
18 */
19export function chain(...callbacks: any[]): (...args: any[]) => void {
20 return (...args: any[]) => {
21 for (const callback of callbacks) {
22 if (typeof callback === 'function') {
23 try {
24 callback(...args);
25 } catch (e) {
26 console.error(e);
27 }
28 }
29 }
30 };
31}
32
33interface Props {
34 [key: string]: any;
35}
36
37// taken from: https://stackoverflow.com/questions/51603250/typescript-3-parameter-list-intersection-type/51604379#51604379
38type TupleTypes<T> = { [P in keyof T]: T[P] } extends { [key: number]: infer V } ? V : never;
39type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
40 ? I
41 : never;
42
43/**
44 * Merges multiple props objects together. Event handlers are chained,
45 * classNames are combined, and ids are deduplicated - different ids
46 * will trigger a side-effect and re-render components hooked up with `useId`.
47 * For all other props, the last prop object overrides all previous ones.
48 * @param args - Multiple sets of props to merge together.
49 * @internal
50 */
51export function mergeProps<T extends Props[]>(...args: T): UnionToIntersection<TupleTypes<T>> {
52 // Start with a base clone of the first argument. This is a lot faster than starting
53 // with an empty object and adding properties as we go.
54 const result: Props = { ...args[0] };
55 for (let i = 1; i < args.length; i++) {
56 const props = args[i];
57 for (const key in props) {
58 const a = result[key];
59 const b = props[key];
60
61 // Chain events
62 if (
63 typeof a === 'function' &&
64 typeof b === 'function' &&
65 // This is a lot faster than a regex.
66 key[0] === 'o' &&
67 key[1] === 'n' &&
68 key.charCodeAt(2) >= /* 'A' */ 65 &&
69 key.charCodeAt(2) <= /* 'Z' */ 90
70 ) {
71 result[key] = chain(a, b);
72
73 // Merge classnames, sometimes classNames are empty string which eval to false, so we just need to do a type check
74 } else if (
75 (key === 'className' || key === 'UNSAFE_className') &&
76 typeof a === 'string' &&
77 typeof b === 'string'
78 ) {
79 result[key] = clsx(a, b);
80 } else {
81 result[key] = b !== undefined ? b : a;
82 }
83 }
84 }
85
86 return result as UnionToIntersection<TupleTypes<T>>;
87}
Added view/app/src/vendor/livekit/react/utils.ts view/app/src/vendor/livekit/react/utils.ts +75 −0 View view/app/src/vendor/livekit/react/utils.ts
@@ -0,0 +1,75 @@
1import * as React from 'react';
2import { mergeProps as mergePropsReactAria } from './mergeProps';
3import { log } from '@livekit/components-core';
4import clsx from 'clsx';
5
6/** @internal */
7export function isProp<U extends HTMLElement, T extends React.HTMLAttributes<U>>(
8 prop: T | undefined,
9): prop is T {
10 return prop !== undefined;
11}
12
13/** @internal */
14export function mergeProps<
15 U extends HTMLElement,
16 T extends Array<React.HTMLAttributes<U> | undefined>,
17>(...props: T) {
18 return mergePropsReactAria(...props.filter(isProp));
19}
20
21/** @internal */
22export function cloneSingleChild(
23 children: React.ReactNode | React.ReactNode[],
24 props?: Record<string, any>,
25 key?: any,
26) {
27 return React.Children.map(children, (child) => {
28 // Checking isValidElement is the safe way and avoids a typescript
29 // error too.
30 if (React.isValidElement(child) && React.Children.only(children)) {
31 const childProps = child.props as { className?: string; style?: React.CSSProperties };
32 if (childProps.className) {
33 // make sure we retain classnames of both passed props and child
34 props ??= {};
35 props.className = clsx(childProps.className, props.className);
36 props.style = { ...childProps.style, ...props.style };
37 }
38 return React.cloneElement(child, { ...props, key });
39 }
40 return child;
41 });
42}
43
44/**
45 * @internal
46 */
47export function warnAboutMissingStyles(el?: HTMLElement) {
48 if (
49 typeof window !== 'undefined' &&
50 import.meta.env.DEV
51 ) {
52 const target = el ?? document.querySelector('.lk-room-container');
53 if (target && !getComputedStyle(target).getPropertyValue('--lk-has-imported-styles')) {
54 log.warn(
55 "It looks like you're not using the `@livekit/components-styles package`. To render the UI with the default styling, please import it in your layout or page.",
56 );
57 }
58 }
59}
60
61/**
62 *
63 * @internal
64 * used to stringify room options to detect dependency changes for react hooks.
65 * Replaces processors and e2ee/encryption options with strings.
66 */
67export function roomOptionsStringifyReplacer(key: string, val: unknown) {
68 if (key === 'processor' && val && typeof val === 'object' && 'name' in val) {
69 return val.name;
70 }
71 if ((key === 'e2ee' || key === 'encryption') && val) {
72 return 'e2ee-enabled';
73 }
74 return val;
75}
Added view/app/src/vendor/livekit/styles/dist/general/components/controls/index.css view/app/src/vendor/livekit/styles/dist/general/components/controls/index.css +3 −0 View view/app/src/vendor/livekit/styles/dist/general/components/controls/index.css
@@ -0,0 +1,3 @@
1.lk-button,.lk-start-audio-button,.lk-chat-toggle,.lk-disconnect-button{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:.5rem;padding:.625rem 1rem;color:var(--lk-control-fg);background-image:none;background-color:var(--lk-control-bg);border:0;border-radius:var(--lk-border-radius);cursor:pointer;white-space:nowrap;font-size:inherit;line-height:inherit;user-select:none}.lk-button:not(:disabled):hover,.lk-start-audio-button:not(:disabled):hover,.lk-chat-toggle:not(:disabled):hover,.lk-disconnect-button:not(:disabled):hover{background-color:var(--lk-control-hover-bg)}.lk-button>svg,.lk-start-audio-button>svg,.lk-chat-toggle>svg,.lk-disconnect-button>svg{overflow:visible}.lk-button[aria-pressed=true],[aria-pressed=true].lk-start-audio-button,[aria-pressed=true].lk-chat-toggle,[aria-pressed=true].lk-disconnect-button{background-color:var(--lk-control-active-bg)}.lk-button[aria-pressed=true]:hover,[aria-pressed=true].lk-start-audio-button:hover,[aria-pressed=true].lk-chat-toggle:hover,[aria-pressed=true].lk-disconnect-button:hover{background-color:var(--lk-control-active-hover-bg)}.lk-button[data-lk-source=screen_share][data-lk-enabled=true],[data-lk-source=screen_share][data-lk-enabled=true].lk-start-audio-button,[data-lk-source=screen_share][data-lk-enabled=true].lk-chat-toggle,[data-lk-source=screen_share][data-lk-enabled=true].lk-disconnect-button{background-color:var(--lk-accent-bg)}.lk-button[data-lk-source=screen_share][data-lk-enabled=true]:hover,[data-lk-source=screen_share][data-lk-enabled=true].lk-start-audio-button:hover,[data-lk-source=screen_share][data-lk-enabled=true].lk-chat-toggle:hover,[data-lk-source=screen_share][data-lk-enabled=true].lk-disconnect-button:hover{background-color:var(--lk-accent2)}.lk-button:disabled,.lk-start-audio-button:disabled,.lk-chat-toggle:disabled,.lk-disconnect-button:disabled{opacity:.5}.lk-button-group{display:inline-flex;align-items:stretch;height:100%}.lk-button-group>.lk-button:first-child,.lk-button-group>.lk-start-audio-button:first-child,.lk-button-group>.lk-chat-toggle:first-child,.lk-button-group>.lk-disconnect-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.lk-button-group-menu{position:relative;flex-shrink:0}.lk-button-group-menu>.lk-button,.lk-button-group-menu>.lk-start-audio-button,.lk-button-group-menu>.lk-chat-toggle,.lk-button-group-menu>.lk-disconnect-button{height:100%;border-top-left-radius:0;border-bottom-left-radius:0}.lk-button-group-menu>.lk-button::after,.lk-button-group-menu>.lk-start-audio-button::after,.lk-button-group-menu>.lk-chat-toggle::after,.lk-button-group-menu>.lk-disconnect-button::after{margin-left:0}.lk-button-menu::after{display:inline-block;content:"";width:.5em;height:.5em;margin-top:-0.25rem;margin-left:.5rem;border-left:.125em solid;border-bottom:.125em solid;transform:rotate(-45deg);transform-origin:center center}.lk-disconnect-button{font-weight:600;color:var(--lk-danger);border:1px solid var(--lk-danger)}.lk-disconnect-button:not(:disabled):hover{--lk-control-hover-bg: var(--lk-danger2);color:var(--lk-danger-fg)}.lk-disconnect-button:not(:disabled):active{--lk-control-hover-bg: var(--lk-danger3);color:var(--lk-danger-fg)}.lk-chat-toggle{position:relative}.lk-chat-toggle[data-lk-unread-msgs]:not([data-lk-unread-msgs="0"]):after{content:attr(data-lk-unread-msgs);position:absolute;top:0;left:0;padding:.25rem;margin-left:.25rem;margin-top:.25rem;border-radius:50%;font-size:.5rem;line-height:.75;background:var(--lk-accent-bg)}.lk-media-device-select:not(:last-child){padding-bottom:.5rem;margin-bottom:.75rem;border-bottom:1px solid var(--lk-border-color)}.lk-media-device-select li:not(:last-child){margin-bottom:.25rem}.lk-media-device-select li>.lk-button{width:100%;justify-content:start;padding-block:.5rem}.lk-media-device-select li:not([data-lk-active=true])>.lk-button:not(:disabled):hover{background-color:var(--lk-bg3)}.lk-media-device-select [data-lk-active=false]>.lk-button:hover{cursor:pointer;background-color:rgba(0,0,0,.05)}.lk-media-device-select [data-lk-active=true]>.lk-button{color:var(--lk-accent-fg);background-color:var(--lk-accent-bg)}.lk-device-menu{width:max-content;position:absolute;top:0;left:0;z-index:5;min-width:10rem;padding:.5rem;margin-bottom:.25rem;white-space:nowrap;background-color:var(--lk-bg2);border:1px solid var(--lk-border-color);border-radius:.75rem;box-shadow:var(--lk-box-shadow)}.lk-device-menu-heading{padding:.25rem .5rem;font-weight:bold;opacity:.65}.lk-start-audio-button{color:var(--lk-accent-fg);background-color:var(--lk-accent-bg)}@media screen and (max-width: 600px){.lk-start-audio-button{position:fixed;top:50%;left:50%;transform:translate(-50%, -50%)}}.lk-pagination-control{position:absolute;bottom:1rem;left:50%;transform:translateX(-50%);display:flex;align-items:stretch;background-color:var(--lk-control-bg);border-radius:var(--lk-border-radius);transition:opacity ease-in-out .15s;opacity:0}.lk-pagination-control:hover{opacity:1}.lk-pagination-control>.lk-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.lk-pagination-control>.lk-button:first-child>svg{transform:rotate(180deg)}.lk-pagination-control>.lk-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.lk-pagination-count{padding:.5rem .875rem;border-inline:1px solid var(--lk-bg)}[data-lk-user-interaction=true].lk-pagination-control{opacity:1}.lk-pagination-indicator{position:absolute;height:var(--lk-grid-gap);background-color:var(--lk-bg2);width:fit-content;padding:.2rem .5rem;bottom:calc(var(--lk-grid-gap)/2);left:50%;transform:translateX(-50%);border-radius:2rem;opacity:1;display:flex;gap:.2rem;align-items:center}.lk-pagination-indicator span{display:inline-block;width:.4rem;height:.4rem;border-radius:9999999px;background-color:var(--lk-fg);opacity:.35;transition:opacity linear .2s}.lk-pagination-indicator span[data-lk-active]{opacity:.9}
2
3/*# sourceMappingURL=index.css.map */
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/components/controls/index.css.map view/app/src/vendor/livekit/styles/dist/general/components/controls/index.css.map +1 −0 View view/app/src/vendor/livekit/styles/dist/general/components/controls/index.css.map
@@ -0,0 +1 @@
1{"version":3,"sources":["../../../../scss/components/controls/_button.scss","../../../../scss/components/controls/_disconnect-button.scss","../../../../scss/components/controls/_chat-toggle.scss","../../../../scss/components/controls/_media-device-select.scss","../../../../scss/components/controls/_start-audio.scss","../../../../scss/components/controls/_pagination-control.scss","../../../../scss/components/controls/_pagination-indicator.scss"],"names":[],"mappings":"AAAA,wEACE,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,sBAAA,CACA,SAAA,CACA,oBAAA,CACA,0BAAA,CACA,qBAAA,CACA,qCAAA,CACA,QAAA,CACA,qCAAA,CACA,cAAA,CACA,kBAAA,CACA,iBAAA,CACA,mBAAA,CACA,gBAAA,CAEA,4JACE,2CAAA,CAGF,wFACE,gBAAA,CAGF,oJACE,4CAAA,CAEA,4KACE,kDAAA,CAKJ,oRACE,oCAAA,CACA,4SACE,kCAAA,CAIJ,4GACE,UAAA,CAIJ,iBACE,mBAAA,CACA,mBAAA,CACA,WAAA,CAEA,4LACE,yBAAA,CACA,4BAAA,CAIJ,sBACE,iBAAA,CACA,aAAA,CAEA,gKACE,WAAA,CACA,wBAAA,CACA,2BAAA,CAEA,4LACE,aAAA,CAOJ,uBACE,oBAAA,CACA,UAAA,CACA,UAAA,CACA,WAAA,CACA,mBAAA,CACA,iBAAA,CACA,wBAAA,CACA,0BAAA,CACA,wBAAA,CACA,8BAAA,CCnFJ,sBAIE,eAAA,CACA,sBAAA,CACA,iCAAA,CAEA,2CACE,wCAAA,CACA,yBAAA,CAEF,4CACE,wCAAA,CACA,yBAAA,CCdJ,gBAEE,iBAAA,CAGF,0EACE,iCAAA,CACA,iBAAA,CACA,KAAA,CACA,MAAA,CACA,cAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,eAAA,CACA,eAAA,CACA,8BAAA,CCjBA,yCACE,oBAAA,CACA,oBAAA,CACA,8CAAA,CAIA,4CACE,oBAAA,CAGF,sCACE,UAAA,CACA,qBAAA,CACA,mBAAA,CAGF,sFACE,8BAAA,CAKF,gEACE,cAAA,CACA,gCAAA,CAKF,yDACE,yBAAA,CACA,oCAAA,CAKN,gBACE,iBAAA,CACA,iBAAA,CACA,KAAA,CACA,MAAA,CACA,SAAA,CACA,eAAA,CACA,aAAA,CACA,oBAAA,CACA,kBAAA,CACA,8BAAA,CACA,uCAAA,CACA,oBAAA,CACA,+BAAA,CAGF,wBACE,oBAAA,CACA,gBAAA,CACA,WAAA,CCvDF,uBAEE,yBAAA,CACA,oCAAA,CAEA,qCALF,uBAMI,cAAA,CACA,OAAA,CACA,QAAA,CACA,+BAAA,CAAA,CCXJ,uBACE,iBAAA,CACA,WAAA,CACA,QAAA,CACA,0BAAA,CACA,YAAA,CACA,mBAAA,CACA,qCAAA,CACA,qCAAA,CACA,mCAAA,CACA,SAAA,CAEA,6BACE,SAAA,CAGF,8CACE,yBAAA,CACA,4BAAA,CAEA,kDACE,wBAAA,CAGJ,6CACE,wBAAA,CACA,2BAAA,CAIJ,qBACE,qBAAA,CACA,oCAAA,CAGF,sDACE,SAAA,CCpCF,yBACE,iBAAA,CACA,yBAAA,CACA,8BAAA,CACA,iBAAA,CACA,mBAAA,CACA,iCAAA,CACA,QAAA,CACA,0BAAA,CACA,kBAAA,CACA,SAAA,CACA,YAAA,CACA,SAAA,CACA,kBAAA,CACA,8BACE,oBAAA,CACA,WAAA,CACA,YAAA,CACA,uBAAA,CACA,6BAAA,CACA,WAAA,CACA,6BAAA,CAEA,8CACE,UAAA","file":"index.css"}
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/components/layout/index.css view/app/src/vendor/livekit/styles/dist/general/components/layout/index.css +3 −0 View view/app/src/vendor/livekit/styles/dist/general/components/layout/index.css
@@ -0,0 +1,3 @@
1.lk-grid-layout{--lk-col-count: 1;--lk-row-count: 1;display:grid;grid-template-columns:repeat(var(--lk-col-count), minmax(0, 1fr));grid-auto-rows:minmax(0, 1fr);grid-gap:var(--lk-grid-gap);width:100%;height:100%;max-width:100%;max-height:100%;padding:var(--lk-grid-gap)}.lk-grid-layout[data-lk-pagination=true]{padding-bottom:calc(var(--lk-grid-gap)*2)}.lk-focus-layout{display:grid;grid-template-columns:1fr 5fr;gap:var(--lk-grid-gap);width:100%;max-height:100%;padding:var(--lk-grid-gap)}.lk-focused-participant{position:relative}.lk-focused-participant .lk-pip-track{position:absolute;top:10px;right:10px;width:20%;height:auto}@media(max-width: 600px){.lk-focus-layout{grid-template-columns:1fr;grid-template-rows:5fr 1fr}.lk-carousel{order:1}}.lk-carousel{max-height:100%;display:flex;gap:var(--lk-grid-gap)}.lk-carousel>*{flex-shrink:0;aspect-ratio:16/10;scroll-snap-align:start}.lk-carousel[data-lk-orientation=vertical]{flex-direction:column;scroll-snap-type:y mandatory;overflow-y:auto;overflow-x:hidden}.lk-carousel[data-lk-orientation=vertical]>*{--lk-height-minus-gaps: calc(100% - calc(var(--lk-grid-gap) * calc(var(--lk-max-visible-tiles) - 1)));height:calc(var(--lk-height-minus-gaps)/var(--lk-max-visible-tiles))}.lk-carousel[data-lk-orientation=horizontal]{scroll-snap-type:x mandatory;overflow-y:hidden;overflow-x:auto}.lk-carousel[data-lk-orientation=horizontal]>*{--lk-width-minus-gaps: calc(100% - var(--lk-grid-gap) * (var(--lk-max-visible-tiles) - 1));width:calc(var(--lk-width-minus-gaps)/var(--lk-max-visible-tiles))}
2
3/*# sourceMappingURL=index.css.map */
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/components/layout/index.css.map view/app/src/vendor/livekit/styles/dist/general/components/layout/index.css.map +1 −0 View view/app/src/vendor/livekit/styles/dist/general/components/layout/index.css.map
@@ -0,0 +1 @@
1{"version":3,"sources":["../../../../scss/components/layout/_grid-layout.scss","../../../../scss/components/layout/_focus-layout.scss","../../../../scss/components/layout/_carousel-view.scss"],"names":[],"mappings":"AAAA,gBACE,iBAAA,CACA,iBAAA,CACA,YAAA,CACA,iEAAA,CACA,6BAAA,CACA,2BAAA,CACA,UAAA,CACA,WAAA,CACA,cAAA,CACA,eAAA,CACA,0BAAA,CAEA,yCACE,yCAAA,CCdJ,iBACE,YAAA,CACA,6BAAA,CACA,sBAAA,CACA,UAAA,CACA,eAAA,CACA,0BAAA,CAGF,wBACE,iBAAA,CAEA,sCACE,iBAAA,CACA,QAAA,CACA,UAAA,CACA,SAAA,CACA,WAAA,CAIJ,yBACE,iBACE,yBAAA,CACA,0BAAA,CAEF,aACE,OAAA,CAAA,CC3BJ,aACE,eAAA,CACA,YAAA,CACA,sBAAA,CAEA,eAEE,aAAA,CACA,kBAAA,CACA,uBAAA,CAGF,2CACE,qBAAA,CACA,4BAAA,CACA,eAAA,CACA,iBAAA,CAEA,6CACE,qGAAA,CACA,oEAAA,CAIJ,6CACE,4BAAA,CACA,iBAAA,CACA,eAAA,CAEA,+CACE,0FAAA,CACA,kEAAA","file":"index.css"}
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/components/participant/index.css view/app/src/vendor/livekit/styles/dist/general/components/participant/index.css +3 −0 View view/app/src/vendor/livekit/styles/dist/general/components/participant/index.css
@@ -0,0 +1,3 @@
1.lk-connection-quality{width:1.5rem;height:1.5rem}.lk-track-muted-indicator-camera,.lk-track-muted-indicator-microphone{position:relative;width:var(--lk-indicator-size, 1rem);height:var(--lk-indicator-size, 1rem);margin-inline-end:.25rem;transition:opacity .25s ease-in-out}.lk-track-muted-indicator-camera[data-lk-muted=true]{opacity:.5}.lk-track-muted-indicator-microphone{--lk-bg: var(--lk-icon-mic)}.lk-track-muted-indicator-microphone[data-lk-muted=true]{opacity:.5}.lk-participant-name{font-size:.875rem}.lk-participant-media-video{width:100%;height:100%;object-fit:cover;object-position:center;background-color:#000}.lk-participant-media-video[data-lk-orientation=landscape]{object-fit:cover}.lk-participant-media-video[data-lk-orientation=portrait],.lk-participant-media-video[data-lk-source=screen_share]{object-fit:contain;background-color:var(--lk-bg2)}.lk-participant-media-audio{width:auto}[data-lk-facing-mode=user] .lk-participant-media-video[data-lk-local-participant=true][data-lk-source=camera]{transform:rotateY(180deg)}.lk-audio-visualizer{width:100%;height:100%;min-height:160px;background:var(--lk-bg-control);aspect-ratio:16/9;border-radius:.5rem;display:flex;justify-content:space-around;align-items:center}.lk-audio-visualizer>rect{fill:var(--lk-accent-bg);transition:transform 100ms cubic-bezier(0.19, 0.02, 0.09, 1)}.lk-audio-visualizer>path{stroke:var(--lk-accent-bg);transition:100ms cubic-bezier(0.19, 0.02, 0.09, 1)}.lk-audio-bar-visualizer{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:var(--lk-bg);gap:var(--lk-va-bar-gap, 24px)}.lk-audio-bar-visualizer>.lk-audio-bar{transform-origin:"center";height:100%;width:var(--lk-va-bar-width, 12px);border-radius:var(--lk-va-bar-border-radius, 32px);background-color:var(--lk-va-bar-bg, rgba(136, 136, 136, 0.2));transition:background-color .25s ease-out}.lk-audio-bar-visualizer[data-lk-va-state=speaking]>.lk-audio-bar,.lk-audio-bar-visualizer>.lk-audio-bar.lk-highlighted,.lk-audio-bar-visualizer>[data-lk-highlighted=true]{background-color:var(--lk-fg, rgb(136, 136, 136));transition:none}.lk-audio-bar-visualizer[data-lk-va-state=thinking]{transition:background-color .15s ease-out}.lk-participant-tile{--lk-speaking-indicator-width: 2.5px;position:relative;display:flex;flex-direction:column;gap:.375rem;overflow:hidden;border-radius:var(--lk-border-radius)}.lk-participant-tile::after{content:"";position:absolute;top:0;bottom:0;left:0;right:0;border-radius:var(--lk-border-radius);border:0px solid var(--lk-accent-bg);transition-property:border opacity;transition-delay:.5s;transition-duration:.4s;pointer-events:none}.lk-participant-tile[data-lk-speaking=true]:not([data-lk-source=screen_share])::after{transition-delay:0s;transition-duration:.2s;border-width:var(--lk-speaking-indicator-width)}.lk-participant-tile .lk-focus-toggle-button{position:absolute;top:.25rem;right:.25rem;padding:.25rem;background-color:rgba(0,0,0,.5);border-radius:calc(var(--lk-border-radius)/2);opacity:0;transition:opacity .2s ease-in-out;transition-delay:.2s}.lk-participant-tile:hover .lk-focus-toggle-button,.lk-participant-tile:focus .lk-focus-toggle-button{opacity:1;transition-delay:0}.lk-participant-tile .lk-connection-quality{opacity:0;transition:opacity .2s ease-in-out;transition-delay:.2s}.lk-participant-tile .lk-connection-quality[data-lk-quality=poor]{opacity:1;transition-delay:0}.lk-participant-tile:hover .lk-connection-quality,.lk-participant-tile:focus .lk-connection-quality{opacity:1;transition-delay:0}.lk-participant-tile .lk-participant-placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background-color:var(--lk-bg2);opacity:0;transition:opacity .2s ease-in-out;pointer-events:none;border-radius:var(--lk-border-radius)}.lk-participant-tile .lk-participant-placeholder svg{height:100%;width:auto;padding:10%}.lk-participant-tile[data-lk-video-muted=true][data-lk-source=camera] .lk-participant-placeholder{opacity:1}.lk-participant-metadata{position:absolute;right:.25rem;bottom:.25rem;left:.25rem;display:flex;flex-direction:row;align-items:center;justify-content:space-between;gap:.5rem;line-height:1}.lk-participant-metadata-item{display:flex;align-items:center;padding:.25rem;background-color:rgba(0,0,0,.5);border-radius:calc(var(--lk-border-radius)/2)}
2
3/*# sourceMappingURL=index.css.map */
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/components/participant/index.css.map view/app/src/vendor/livekit/styles/dist/general/components/participant/index.css.map +1 −0 View view/app/src/vendor/livekit/styles/dist/general/components/participant/index.css.map
@@ -0,0 +1 @@
1{"version":3,"sources":["../../../../scss/components/participant/_connection-quality.scss","../../../../scss/components/participant/_track-muted-indicator.scss","../../../../scss/components/participant/_participant-name.scss","../../../../scss/components/participant/_participant-media.scss","../../../../scss/components/participant/_audio-visualizer.scss","../../../../scss/components/participant/_participant-tile.scss"],"names":[],"mappings":"AAAA,uBACE,YAAA,CACA,aAAA,CCFF,sEAEE,iBAAA,CACA,oCAAA,CACA,qCAAA,CACA,wBAAA,CAGA,mCAAA,CAMA,qDACE,UAAA,CAIJ,qCACE,2BAAA,CAEA,yDACE,UAAA,CCvBJ,qBACE,iBAAA,CCDF,4BACE,UAAA,CACA,WAAA,CACA,gBAAA,CACA,sBAAA,CAEA,qBAAA,CAEA,2DACE,gBAAA,CAIF,mHAEE,kBAAA,CACA,8BAAA,CAIJ,4BACE,UAAA,CAIF,8GAEE,yBAAA,CCxBF,qBACE,UAAA,CACA,WAAA,CACA,gBAAA,CACA,+BAAA,CACA,iBAAA,CAEA,mBAAA,CACA,YAAA,CACA,4BAAA,CACA,kBAAA,CAGA,0BACE,wBAAA,CAGA,4DAAA,CAGF,0BACE,0BAAA,CAGA,kDAAA,CAIJ,yBACE,YAAA,CACA,kBAAA,CACA,sBAAA,CACA,UAAA,CACA,WAAA,CACA,uBAAA,CACA,8BAAA,CAEA,uCACE,yBAAA,CACA,WAAA,CACA,kCAAA,CACA,kDAAA,CACA,8DAAA,CACA,yCAAA,CAIF,4KAGE,iDAAA,CACA,eAAA,CASF,oDACE,yCAAA,CChEJ,qBACE,oCAAA,CACA,iBAAA,CACA,YAAA,CACA,qBAAA,CACA,WAAA,CACA,eAAA,CACA,qCAAA,CACA,4BACE,UAAA,CACA,iBAAA,CACA,KAAA,CACA,QAAA,CACA,MAAA,CACA,OAAA,CACA,qCAAA,CACA,oCAAA,CAEA,kCAAA,CACA,oBAAA,CACA,uBAAA,CACA,mBAAA,CAEF,sFAEE,mBAAA,CACA,uBAAA,CACA,+CAAA,CAGF,6CACE,iBAAA,CACA,UAAA,CACA,YAAA,CACA,cAAA,CACA,+BAAA,CACA,6CAAA,CACA,SAAA,CACA,kCAAA,CACA,oBAAA,CAGF,sGAEE,SAAA,CACA,kBAAA,CAGF,4CACE,SAAA,CACA,kCAAA,CACA,oBAAA,CAEA,kEACE,SAAA,CACA,kBAAA,CAIJ,oGAEE,SAAA,CACA,kBAAA,CAGF,iDACE,iBAAA,CACA,OAAA,CACA,YAAA,CACA,kBAAA,CACA,sBAAA,CACA,8BAAA,CACA,SAAA,CACA,kCAAA,CACA,mBAAA,CACA,qCAAA,CAEA,qDACE,WAAA,CACA,UAAA,CACA,WAAA,CAIJ,kGACE,SAAA,CAIJ,yBACE,iBAAA,CACA,YAAA,CACA,aAAA,CACA,WAAA,CACA,YAAA,CACA,kBAAA,CACA,kBAAA,CACA,6BAAA,CACA,SAAA,CACA,aAAA,CAGF,8BACE,YAAA,CACA,kBAAA,CACA,cAAA,CACA,+BAAA,CACA,6CAAA","file":"index.css"}
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/components/index.css view/app/src/vendor/livekit/styles/dist/general/components/index.css +3 −0 View view/app/src/vendor/livekit/styles/dist/general/components/index.css
@@ -0,0 +1,3 @@
1.lk-button,.lk-start-audio-button,.lk-chat-toggle,.lk-disconnect-button{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:.5rem;padding:.625rem 1rem;color:var(--lk-control-fg);background-image:none;background-color:var(--lk-control-bg);border:0;border-radius:var(--lk-border-radius);cursor:pointer;white-space:nowrap;font-size:inherit;line-height:inherit;user-select:none}.lk-button:not(:disabled):hover,.lk-start-audio-button:not(:disabled):hover,.lk-chat-toggle:not(:disabled):hover,.lk-disconnect-button:not(:disabled):hover{background-color:var(--lk-control-hover-bg)}.lk-button>svg,.lk-start-audio-button>svg,.lk-chat-toggle>svg,.lk-disconnect-button>svg{overflow:visible}.lk-button[aria-pressed=true],[aria-pressed=true].lk-start-audio-button,[aria-pressed=true].lk-chat-toggle,[aria-pressed=true].lk-disconnect-button{background-color:var(--lk-control-active-bg)}.lk-button[aria-pressed=true]:hover,[aria-pressed=true].lk-start-audio-button:hover,[aria-pressed=true].lk-chat-toggle:hover,[aria-pressed=true].lk-disconnect-button:hover{background-color:var(--lk-control-active-hover-bg)}.lk-button[data-lk-source=screen_share][data-lk-enabled=true],[data-lk-source=screen_share][data-lk-enabled=true].lk-start-audio-button,[data-lk-source=screen_share][data-lk-enabled=true].lk-chat-toggle,[data-lk-source=screen_share][data-lk-enabled=true].lk-disconnect-button{background-color:var(--lk-accent-bg)}.lk-button[data-lk-source=screen_share][data-lk-enabled=true]:hover,[data-lk-source=screen_share][data-lk-enabled=true].lk-start-audio-button:hover,[data-lk-source=screen_share][data-lk-enabled=true].lk-chat-toggle:hover,[data-lk-source=screen_share][data-lk-enabled=true].lk-disconnect-button:hover{background-color:var(--lk-accent2)}.lk-button:disabled,.lk-start-audio-button:disabled,.lk-chat-toggle:disabled,.lk-disconnect-button:disabled{opacity:.5}.lk-button-group{display:inline-flex;align-items:stretch;height:100%}.lk-button-group>.lk-button:first-child,.lk-button-group>.lk-start-audio-button:first-child,.lk-button-group>.lk-chat-toggle:first-child,.lk-button-group>.lk-disconnect-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.lk-button-group-menu{position:relative;flex-shrink:0}.lk-button-group-menu>.lk-button,.lk-button-group-menu>.lk-start-audio-button,.lk-button-group-menu>.lk-chat-toggle,.lk-button-group-menu>.lk-disconnect-button{height:100%;border-top-left-radius:0;border-bottom-left-radius:0}.lk-button-group-menu>.lk-button::after,.lk-button-group-menu>.lk-start-audio-button::after,.lk-button-group-menu>.lk-chat-toggle::after,.lk-button-group-menu>.lk-disconnect-button::after{margin-left:0}.lk-button-menu::after{display:inline-block;content:"";width:.5em;height:.5em;margin-top:-0.25rem;margin-left:.5rem;border-left:.125em solid;border-bottom:.125em solid;transform:rotate(-45deg);transform-origin:center center}.lk-disconnect-button{font-weight:600;color:var(--lk-danger);border:1px solid var(--lk-danger)}.lk-disconnect-button:not(:disabled):hover{--lk-control-hover-bg: var(--lk-danger2);color:var(--lk-danger-fg)}.lk-disconnect-button:not(:disabled):active{--lk-control-hover-bg: var(--lk-danger3);color:var(--lk-danger-fg)}.lk-chat-toggle{position:relative}.lk-chat-toggle[data-lk-unread-msgs]:not([data-lk-unread-msgs="0"]):after{content:attr(data-lk-unread-msgs);position:absolute;top:0;left:0;padding:.25rem;margin-left:.25rem;margin-top:.25rem;border-radius:50%;font-size:.5rem;line-height:.75;background:var(--lk-accent-bg)}.lk-media-device-select:not(:last-child){padding-bottom:.5rem;margin-bottom:.75rem;border-bottom:1px solid var(--lk-border-color)}.lk-media-device-select li:not(:last-child){margin-bottom:.25rem}.lk-media-device-select li>.lk-button{width:100%;justify-content:start;padding-block:.5rem}.lk-media-device-select li:not([data-lk-active=true])>.lk-button:not(:disabled):hover{background-color:var(--lk-bg3)}.lk-media-device-select [data-lk-active=false]>.lk-button:hover{cursor:pointer;background-color:rgba(0,0,0,.05)}.lk-media-device-select [data-lk-active=true]>.lk-button{color:var(--lk-accent-fg);background-color:var(--lk-accent-bg)}.lk-device-menu{width:max-content;position:absolute;top:0;left:0;z-index:5;min-width:10rem;padding:.5rem;margin-bottom:.25rem;white-space:nowrap;background-color:var(--lk-bg2);border:1px solid var(--lk-border-color);border-radius:.75rem;box-shadow:var(--lk-box-shadow)}.lk-device-menu-heading{padding:.25rem .5rem;font-weight:bold;opacity:.65}.lk-start-audio-button{color:var(--lk-accent-fg);background-color:var(--lk-accent-bg)}@media screen and (max-width: 600px){.lk-start-audio-button{position:fixed;top:50%;left:50%;transform:translate(-50%, -50%)}}.lk-pagination-control{position:absolute;bottom:1rem;left:50%;transform:translateX(-50%);display:flex;align-items:stretch;background-color:var(--lk-control-bg);border-radius:var(--lk-border-radius);transition:opacity ease-in-out .15s;opacity:0}.lk-pagination-control:hover{opacity:1}.lk-pagination-control>.lk-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.lk-pagination-control>.lk-button:first-child>svg{transform:rotate(180deg)}.lk-pagination-control>.lk-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.lk-pagination-count{padding:.5rem .875rem;border-inline:1px solid var(--lk-bg)}[data-lk-user-interaction=true].lk-pagination-control{opacity:1}.lk-pagination-indicator{position:absolute;height:var(--lk-grid-gap);background-color:var(--lk-bg2);width:fit-content;padding:.2rem .5rem;bottom:calc(var(--lk-grid-gap)/2);left:50%;transform:translateX(-50%);border-radius:2rem;opacity:1;display:flex;gap:.2rem;align-items:center}.lk-pagination-indicator span{display:inline-block;width:.4rem;height:.4rem;border-radius:9999999px;background-color:var(--lk-fg);opacity:.35;transition:opacity linear .2s}.lk-pagination-indicator span[data-lk-active]{opacity:.9}.lk-grid-layout{--lk-col-count: 1;--lk-row-count: 1;display:grid;grid-template-columns:repeat(var(--lk-col-count), minmax(0, 1fr));grid-auto-rows:minmax(0, 1fr);grid-gap:var(--lk-grid-gap);width:100%;height:100%;max-width:100%;max-height:100%;padding:var(--lk-grid-gap)}.lk-grid-layout[data-lk-pagination=true]{padding-bottom:calc(var(--lk-grid-gap)*2)}.lk-focus-layout{display:grid;grid-template-columns:1fr 5fr;gap:var(--lk-grid-gap);width:100%;max-height:100%;padding:var(--lk-grid-gap)}.lk-focused-participant{position:relative}.lk-focused-participant .lk-pip-track{position:absolute;top:10px;right:10px;width:20%;height:auto}@media(max-width: 600px){.lk-focus-layout{grid-template-columns:1fr;grid-template-rows:5fr 1fr}.lk-carousel{order:1}}.lk-carousel{max-height:100%;display:flex;gap:var(--lk-grid-gap)}.lk-carousel>*{flex-shrink:0;aspect-ratio:16/10;scroll-snap-align:start}.lk-carousel[data-lk-orientation=vertical]{flex-direction:column;scroll-snap-type:y mandatory;overflow-y:auto;overflow-x:hidden}.lk-carousel[data-lk-orientation=vertical]>*{--lk-height-minus-gaps: calc(100% - calc(var(--lk-grid-gap) * calc(var(--lk-max-visible-tiles) - 1)));height:calc(var(--lk-height-minus-gaps)/var(--lk-max-visible-tiles))}.lk-carousel[data-lk-orientation=horizontal]{scroll-snap-type:x mandatory;overflow-y:hidden;overflow-x:auto}.lk-carousel[data-lk-orientation=horizontal]>*{--lk-width-minus-gaps: calc(100% - var(--lk-grid-gap) * (var(--lk-max-visible-tiles) - 1));width:calc(var(--lk-width-minus-gaps)/var(--lk-max-visible-tiles))}.lk-connection-quality{width:1.5rem;height:1.5rem}.lk-track-muted-indicator-camera,.lk-track-muted-indicator-microphone{position:relative;width:var(--lk-indicator-size, 1rem);height:var(--lk-indicator-size, 1rem);margin-inline-end:.25rem;transition:opacity .25s ease-in-out}.lk-track-muted-indicator-camera[data-lk-muted=true]{opacity:.5}.lk-track-muted-indicator-microphone{--lk-bg: var(--lk-icon-mic)}.lk-track-muted-indicator-microphone[data-lk-muted=true]{opacity:.5}.lk-participant-name{font-size:.875rem}.lk-participant-media-video{width:100%;height:100%;object-fit:cover;object-position:center;background-color:#000}.lk-participant-media-video[data-lk-orientation=landscape]{object-fit:cover}.lk-participant-media-video[data-lk-orientation=portrait],.lk-participant-media-video[data-lk-source=screen_share]{object-fit:contain;background-color:var(--lk-bg2)}.lk-participant-media-audio{width:auto}[data-lk-facing-mode=user] .lk-participant-media-video[data-lk-local-participant=true][data-lk-source=camera]{transform:rotateY(180deg)}.lk-audio-visualizer{width:100%;height:100%;min-height:160px;background:var(--lk-bg-control);aspect-ratio:16/9;border-radius:.5rem;display:flex;justify-content:space-around;align-items:center}.lk-audio-visualizer>rect{fill:var(--lk-accent-bg);transition:transform 100ms cubic-bezier(0.19, 0.02, 0.09, 1)}.lk-audio-visualizer>path{stroke:var(--lk-accent-bg);transition:100ms cubic-bezier(0.19, 0.02, 0.09, 1)}.lk-audio-bar-visualizer{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:var(--lk-bg);gap:var(--lk-va-bar-gap, 24px)}.lk-audio-bar-visualizer>.lk-audio-bar{transform-origin:"center";height:100%;width:var(--lk-va-bar-width, 12px);border-radius:var(--lk-va-bar-border-radius, 32px);background-color:var(--lk-va-bar-bg, rgba(136, 136, 136, 0.2));transition:background-color .25s ease-out}.lk-audio-bar-visualizer[data-lk-va-state=speaking]>.lk-audio-bar,.lk-audio-bar-visualizer>.lk-audio-bar.lk-highlighted,.lk-audio-bar-visualizer>[data-lk-highlighted=true]{background-color:var(--lk-fg, rgb(136, 136, 136));transition:none}.lk-audio-bar-visualizer[data-lk-va-state=thinking]{transition:background-color .15s ease-out}.lk-participant-tile{--lk-speaking-indicator-width: 2.5px;position:relative;display:flex;flex-direction:column;gap:.375rem;overflow:hidden;border-radius:var(--lk-border-radius)}.lk-participant-tile::after{content:"";position:absolute;top:0;bottom:0;left:0;right:0;border-radius:var(--lk-border-radius);border:0px solid var(--lk-accent-bg);transition-property:border opacity;transition-delay:.5s;transition-duration:.4s;pointer-events:none}.lk-participant-tile[data-lk-speaking=true]:not([data-lk-source=screen_share])::after{transition-delay:0s;transition-duration:.2s;border-width:var(--lk-speaking-indicator-width)}.lk-participant-tile .lk-focus-toggle-button{position:absolute;top:.25rem;right:.25rem;padding:.25rem;background-color:rgba(0,0,0,.5);border-radius:calc(var(--lk-border-radius)/2);opacity:0;transition:opacity .2s ease-in-out;transition-delay:.2s}.lk-participant-tile:hover .lk-focus-toggle-button,.lk-participant-tile:focus .lk-focus-toggle-button{opacity:1;transition-delay:0}.lk-participant-tile .lk-connection-quality{opacity:0;transition:opacity .2s ease-in-out;transition-delay:.2s}.lk-participant-tile .lk-connection-quality[data-lk-quality=poor]{opacity:1;transition-delay:0}.lk-participant-tile:hover .lk-connection-quality,.lk-participant-tile:focus .lk-connection-quality{opacity:1;transition-delay:0}.lk-participant-tile .lk-participant-placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background-color:var(--lk-bg2);opacity:0;transition:opacity .2s ease-in-out;pointer-events:none;border-radius:var(--lk-border-radius)}.lk-participant-tile .lk-participant-placeholder svg{height:100%;width:auto;padding:10%}.lk-participant-tile[data-lk-video-muted=true][data-lk-source=camera] .lk-participant-placeholder{opacity:1}.lk-participant-metadata{position:absolute;right:.25rem;bottom:.25rem;left:.25rem;display:flex;flex-direction:row;align-items:center;justify-content:space-between;gap:.5rem;line-height:1}.lk-participant-metadata-item{display:flex;align-items:center;padding:.25rem;background-color:rgba(0,0,0,.5);border-radius:calc(var(--lk-border-radius)/2)}.lk-toast{position:fixed;top:.75rem;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:.5rem;padding:.75rem 1.25rem;background-color:var(--lk-bg);border:1px solid var(--lk-border-color);border-radius:var(--lk-border-radius);box-shadow:var(--lk-box-shadow)}.lk-spinner{animation:lk-rotate 2s infinite linear}@keyframes lk-rotate{from{transform:rotate(0deg)}to{transform:rotate(359deg)}}.lk-room-container{background-color:var(--lk-bg);line-height:var(--lk-line-height)}[data-lk-theme]{font-size:var(--lk-font-size);font-family:var(--lk-font-family);color:var(--lk-fg)}[data-lk-theme] .lk-list{list-style:none;margin:0;padding:0}[data-lk-theme] .lk-form-control{font-family:var(--lk-font-family);padding:.625rem 1rem;background-color:var(--lk-control-bg);border:1px solid var(--lk-border-color);border-radius:var(--lk-border-radius)}.lk-room-container{position:relative;width:100%;height:100%;--lk-has-imported-styles: "true"}.lk-room-container *[class^=lk-],.lk-room-container *[class*=" lk-"]{box-sizing:border-box}
2
3/*# sourceMappingURL=index.css.map */
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/components/index.css.map view/app/src/vendor/livekit/styles/dist/general/components/index.css.map +1 −0 View view/app/src/vendor/livekit/styles/dist/general/components/index.css.map
@@ -0,0 +1 @@
1{"version":3,"sources":["../../../scss/components/controls/_button.scss","../../../scss/components/controls/_disconnect-button.scss","../../../scss/components/controls/_chat-toggle.scss","../../../scss/components/controls/_media-device-select.scss","../../../scss/components/controls/_start-audio.scss","../../../scss/components/controls/_pagination-control.scss","../../../scss/components/controls/_pagination-indicator.scss","../../../scss/components/layout/_grid-layout.scss","../../../scss/components/layout/_focus-layout.scss","../../../scss/components/layout/_carousel-view.scss","../../../scss/components/participant/_connection-quality.scss","../../../scss/components/participant/_track-muted-indicator.scss","../../../scss/components/participant/_participant-name.scss","../../../scss/components/participant/_participant-media.scss","../../../scss/components/participant/_audio-visualizer.scss","../../../scss/components/participant/_participant-tile.scss","../../../scss/components/_toast.scss","../../../scss/_preflight.scss","../../../scss/components/_room.scss"],"names":[],"mappings":"AAAA,wEACE,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,sBAAA,CACA,SAAA,CACA,oBAAA,CACA,0BAAA,CACA,qBAAA,CACA,qCAAA,CACA,QAAA,CACA,qCAAA,CACA,cAAA,CACA,kBAAA,CACA,iBAAA,CACA,mBAAA,CACA,gBAAA,CAEA,4JACE,2CAAA,CAGF,wFACE,gBAAA,CAGF,oJACE,4CAAA,CAEA,4KACE,kDAAA,CAKJ,oRACE,oCAAA,CACA,4SACE,kCAAA,CAIJ,4GACE,UAAA,CAIJ,iBACE,mBAAA,CACA,mBAAA,CACA,WAAA,CAEA,4LACE,yBAAA,CACA,4BAAA,CAIJ,sBACE,iBAAA,CACA,aAAA,CAEA,gKACE,WAAA,CACA,wBAAA,CACA,2BAAA,CAEA,4LACE,aAAA,CAOJ,uBACE,oBAAA,CACA,UAAA,CACA,UAAA,CACA,WAAA,CACA,mBAAA,CACA,iBAAA,CACA,wBAAA,CACA,0BAAA,CACA,wBAAA,CACA,8BAAA,CCnFJ,sBAIE,eAAA,CACA,sBAAA,CACA,iCAAA,CAEA,2CACE,wCAAA,CACA,yBAAA,CAEF,4CACE,wCAAA,CACA,yBAAA,CCdJ,gBAEE,iBAAA,CAGF,0EACE,iCAAA,CACA,iBAAA,CACA,KAAA,CACA,MAAA,CACA,cAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,eAAA,CACA,eAAA,CACA,8BAAA,CCjBA,yCACE,oBAAA,CACA,oBAAA,CACA,8CAAA,CAIA,4CACE,oBAAA,CAGF,sCACE,UAAA,CACA,qBAAA,CACA,mBAAA,CAGF,sFACE,8BAAA,CAKF,gEACE,cAAA,CACA,gCAAA,CAKF,yDACE,yBAAA,CACA,oCAAA,CAKN,gBACE,iBAAA,CACA,iBAAA,CACA,KAAA,CACA,MAAA,CACA,SAAA,CACA,eAAA,CACA,aAAA,CACA,oBAAA,CACA,kBAAA,CACA,8BAAA,CACA,uCAAA,CACA,oBAAA,CACA,+BAAA,CAGF,wBACE,oBAAA,CACA,gBAAA,CACA,WAAA,CCvDF,uBAEE,yBAAA,CACA,oCAAA,CAEA,qCALF,uBAMI,cAAA,CACA,OAAA,CACA,QAAA,CACA,+BAAA,CAAA,CCXJ,uBACE,iBAAA,CACA,WAAA,CACA,QAAA,CACA,0BAAA,CACA,YAAA,CACA,mBAAA,CACA,qCAAA,CACA,qCAAA,CACA,mCAAA,CACA,SAAA,CAEA,6BACE,SAAA,CAGF,8CACE,yBAAA,CACA,4BAAA,CAEA,kDACE,wBAAA,CAGJ,6CACE,wBAAA,CACA,2BAAA,CAIJ,qBACE,qBAAA,CACA,oCAAA,CAGF,sDACE,SAAA,CCpCF,yBACE,iBAAA,CACA,yBAAA,CACA,8BAAA,CACA,iBAAA,CACA,mBAAA,CACA,iCAAA,CACA,QAAA,CACA,0BAAA,CACA,kBAAA,CACA,SAAA,CACA,YAAA,CACA,SAAA,CACA,kBAAA,CACA,8BACE,oBAAA,CACA,WAAA,CACA,YAAA,CACA,uBAAA,CACA,6BAAA,CACA,WAAA,CACA,6BAAA,CAEA,8CACE,UAAA,CCxBN,gBACE,iBAAA,CACA,iBAAA,CACA,YAAA,CACA,iEAAA,CACA,6BAAA,CACA,2BAAA,CACA,UAAA,CACA,WAAA,CACA,cAAA,CACA,eAAA,CACA,0BAAA,CAEA,yCACE,yCAAA,CCdJ,iBACE,YAAA,CACA,6BAAA,CACA,sBAAA,CACA,UAAA,CACA,eAAA,CACA,0BAAA,CAGF,wBACE,iBAAA,CAEA,sCACE,iBAAA,CACA,QAAA,CACA,UAAA,CACA,SAAA,CACA,WAAA,CAIJ,yBACE,iBACE,yBAAA,CACA,0BAAA,CAEF,aACE,OAAA,CAAA,CC3BJ,aACE,eAAA,CACA,YAAA,CACA,sBAAA,CAEA,eAEE,aAAA,CACA,kBAAA,CACA,uBAAA,CAGF,2CACE,qBAAA,CACA,4BAAA,CACA,eAAA,CACA,iBAAA,CAEA,6CACE,qGAAA,CACA,oEAAA,CAIJ,6CACE,4BAAA,CACA,iBAAA,CACA,eAAA,CAEA,+CACE,0FAAA,CACA,kEAAA,CC/BN,uBACE,YAAA,CACA,aAAA,CCFF,sEAEE,iBAAA,CACA,oCAAA,CACA,qCAAA,CACA,wBAAA,CAGA,mCAAA,CAMA,qDACE,UAAA,CAIJ,qCACE,2BAAA,CAEA,yDACE,UAAA,CCvBJ,qBACE,iBAAA,CCDF,4BACE,UAAA,CACA,WAAA,CACA,gBAAA,CACA,sBAAA,CAEA,qBAAA,CAEA,2DACE,gBAAA,CAIF,mHAEE,kBAAA,CACA,8BAAA,CAIJ,4BACE,UAAA,CAIF,8GAEE,yBAAA,CCxBF,qBACE,UAAA,CACA,WAAA,CACA,gBAAA,CACA,+BAAA,CACA,iBAAA,CAEA,mBAAA,CACA,YAAA,CACA,4BAAA,CACA,kBAAA,CAGA,0BACE,wBAAA,CAGA,4DAAA,CAGF,0BACE,0BAAA,CAGA,kDAAA,CAIJ,yBACE,YAAA,CACA,kBAAA,CACA,sBAAA,CACA,UAAA,CACA,WAAA,CACA,uBAAA,CACA,8BAAA,CAEA,uCACE,yBAAA,CACA,WAAA,CACA,kCAAA,CACA,kDAAA,CACA,8DAAA,CACA,yCAAA,CAIF,4KAGE,iDAAA,CACA,eAAA,CASF,oDACE,yCAAA,CChEJ,qBACE,oCAAA,CACA,iBAAA,CACA,YAAA,CACA,qBAAA,CACA,WAAA,CACA,eAAA,CACA,qCAAA,CACA,4BACE,UAAA,CACA,iBAAA,CACA,KAAA,CACA,QAAA,CACA,MAAA,CACA,OAAA,CACA,qCAAA,CACA,oCAAA,CAEA,kCAAA,CACA,oBAAA,CACA,uBAAA,CACA,mBAAA,CAEF,sFAEE,mBAAA,CACA,uBAAA,CACA,+CAAA,CAGF,6CACE,iBAAA,CACA,UAAA,CACA,YAAA,CACA,cAAA,CACA,+BAAA,CACA,6CAAA,CACA,SAAA,CACA,kCAAA,CACA,oBAAA,CAGF,sGAEE,SAAA,CACA,kBAAA,CAGF,4CACE,SAAA,CACA,kCAAA,CACA,oBAAA,CAEA,kEACE,SAAA,CACA,kBAAA,CAIJ,oGAEE,SAAA,CACA,kBAAA,CAGF,iDACE,iBAAA,CACA,OAAA,CACA,YAAA,CACA,kBAAA,CACA,sBAAA,CACA,8BAAA,CACA,SAAA,CACA,kCAAA,CACA,mBAAA,CACA,qCAAA,CAEA,qDACE,WAAA,CACA,UAAA,CACA,WAAA,CAIJ,kGACE,SAAA,CAIJ,yBACE,iBAAA,CACA,YAAA,CACA,aAAA,CACA,WAAA,CACA,YAAA,CACA,kBAAA,CACA,kBAAA,CACA,6BAAA,CACA,SAAA,CACA,aAAA,CAGF,8BACE,YAAA,CACA,kBAAA,CACA,cAAA,CACA,+BAAA,CACA,6CAAA,CC3GF,UACE,cAAA,CACA,UAAA,CACA,QAAA,CACA,0BAAA,CACA,YAAA,CACA,kBAAA,CACA,SAAA,CACA,sBAAA,CACA,6BAAA,CACA,uCAAA,CACA,qCAAA,CACA,+BAAA,CAGF,YACE,sCAAA,CAGF,qBACE,KACE,sBAAA,CAEF,GACE,wBAAA,CAAA,CCxBJ,mBACE,6BAAA,CACA,iCAAA,CAGF,gBACE,6BAAA,CACA,iCAAA,CACA,kBAAA,CAEA,yBACE,eAAA,CACA,QAAA,CACA,SAAA,CAGF,iCACE,iCAAA,CACA,oBAAA,CACA,qCAAA,CACA,uCAAA,CACA,qCAAA,CCnBJ,mBAEE,iBAAA,CACA,UAAA,CACA,WAAA,CACA,gCAAA,CAEA,qEAEE,qBAAA","file":"index.css"}
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/prefabs/audio-conference.css view/app/src/vendor/livekit/styles/dist/general/prefabs/audio-conference.css +3 −0 View view/app/src/vendor/livekit/styles/dist/general/prefabs/audio-conference.css
@@ -0,0 +1,3 @@
1.lk-audio-conference{position:relative;width:100%;height:100%}.lk-audio-conference-stage{width:100%;height:100%;display:grid;grid-template-columns:repeat(3, 1fr);gap:10px}
2
3/*# sourceMappingURL=audio-conference.css.map */
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/prefabs/audio-conference.css.map view/app/src/vendor/livekit/styles/dist/general/prefabs/audio-conference.css.map +1 −0 View view/app/src/vendor/livekit/styles/dist/general/prefabs/audio-conference.css.map
@@ -0,0 +1 @@
1{"version":3,"sources":["../../../scss/prefabs/audio-conference.scss"],"names":[],"mappings":"AAAA,qBACE,iBAAA,CACA,UAAA,CACA,WAAA,CAGF,2BACE,UAAA,CACA,WAAA,CACA,YAAA,CACA,oCAAA,CACA,QAAA","file":"audio-conference.css"}
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/prefabs/chat.css view/app/src/vendor/livekit/styles/dist/general/prefabs/chat.css +3 −0 View view/app/src/vendor/livekit/styles/dist/general/prefabs/chat.css
@@ -0,0 +1,3 @@
1.lk-chat{display:grid;grid-template-rows:var(--lk-chat-header-height) 1fr var(--lk-control-bar-height);width:clamp(200px,55ch,60ch);background-color:var(--lk-bg2);border-left:1px solid var(--lk-border-color);align-items:end}.lk-chat-header{height:var(--lk-chat-header-height);padding:.75rem;position:relative;display:flex;align-items:center;justify-content:center}.lk-chat-header .lk-close-button{position:absolute;right:0;transform:translateX(-50%);background-color:rgba(0,0,0,0)}.lk-chat-header .lk-close-button:hover{background-color:var(--lk-control-active-hover-bg)}.lk-chat-messages{display:flex;width:100%;max-height:100%;flex-direction:column;gap:.25rem;overflow:auto}.lk-chat-entry{display:flex;flex-direction:column;gap:.25rem;margin:0 .25rem}.lk-chat-entry .lk-meta-data{font-size:.75rem;color:var(--lk-fg5);white-space:nowrap;padding:0 .3rem;display:flex}.lk-chat-entry .lk-meta-data .lk-participant-name{margin-top:1rem}.lk-chat-entry .lk-meta-data .lk-timestamp{margin-left:auto;align-self:flex-end}.lk-chat-entry .lk-edit-button{background:none;float:right;margin:0;padding:0 .25rem;border-radius:0;font-size:12px}.lk-chat-entry .lk-message-body{display:inline-block;border-radius:15px;padding:.25rem .75rem;word-break:break-word;width:fit-content;max-width:calc(100% - 32px)}.lk-chat-entry[data-lk-message-origin=local] .lk-message-body{background-color:var(--lk-bg5)}.lk-chat-entry[data-lk-message-origin=remote] .lk-message-body{background-color:var(--lk-accent4)}.lk-chat-entry a{text-decoration:underline;color:inherit}.lk-chat-entry *{margin-block-start:.25em;margin-block-end:.25em}.lk-chat-entry:last-child{margin-bottom:.25rem}.lk-chat-form{display:flex;gap:.75rem;padding:.75rem;border-top:1px solid var(--lk-border-color);max-height:var(--lk-control-bar-height)}.lk-chat-form-input{font-size:inherit;line-height:inherit;width:100%}@media(max-width: 600px){.lk-chat{position:fixed;top:0;right:0;max-width:100%;bottom:var(--lk-control-bar-height)}}
2
3/*# sourceMappingURL=chat.css.map */
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/prefabs/chat.css.map view/app/src/vendor/livekit/styles/dist/general/prefabs/chat.css.map +1 −0 View view/app/src/vendor/livekit/styles/dist/general/prefabs/chat.css.map
@@ -0,0 +1 @@
1{"version":3,"sources":["../../../scss/prefabs/chat.scss"],"names":[],"mappings":"AAAA,SACE,YAAA,CACA,gFAAA,CACA,4BAAA,CACA,8BAAA,CACA,4CAAA,CACA,eAAA,CAGF,gBACE,mCAAA,CACA,cAAA,CACA,iBAAA,CACA,YAAA,CACA,kBAAA,CACA,sBAAA,CACA,iCACE,iBAAA,CACA,OAAA,CACA,0BAAA,CACA,8BAAA,CACA,uCACE,kDAAA,CAKN,kBACE,YAAA,CACA,UAAA,CACA,eAAA,CACA,qBAAA,CACA,UAAA,CACA,aAAA,CAGF,eACE,YAAA,CACA,qBAAA,CACA,UAAA,CACA,eAAA,CAEA,6BACE,gBAAA,CACA,mBAAA,CACA,kBAAA,CACA,eAAA,CACA,YAAA,CAEA,kDACE,eAAA,CAGF,2CACE,gBAAA,CACA,mBAAA,CAIJ,+BACE,eAAA,CACA,WAAA,CACA,QAAA,CACA,gBAAA,CACA,eAAA,CACA,cAAA,CAGF,gCACE,oBAAA,CACA,kBAAA,CACA,qBAAA,CACA,qBAAA,CACA,iBAAA,CACA,2BAAA,CAIA,8DACE,8BAAA,CAKF,+DACE,kCAAA,CAIJ,iBACE,yBAAA,CACA,aAAA,CAGF,iBACE,wBAAA,CACA,sBAAA,CAGF,0BACE,oBAAA,CAIJ,cACE,YAAA,CACA,UAAA,CACA,cAAA,CACA,2CAAA,CACA,uCAAA,CAGF,oBACE,iBAAA,CACA,mBAAA,CACA,UAAA,CAGF,yBACE,SACE,cAAA,CACA,KAAA,CACA,OAAA,CACA,cAAA,CACA,mCAAA,CAAA","file":"chat.css"}
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/prefabs/control-bar.css view/app/src/vendor/livekit/styles/dist/general/prefabs/control-bar.css +3 −0 View view/app/src/vendor/livekit/styles/dist/general/prefabs/control-bar.css
@@ -0,0 +1,3 @@
1.lk-control-bar,.lk-agent-control-bar{display:flex;gap:.5rem;align-items:center;justify-content:center;padding:.75rem;border-top:1px solid var(--lk-border-color);max-height:var(--lk-control-bar-height)}.lk-agent-control-bar{height:var(--lk-control-bar-height);--lk-bg: transparent;--lk-va-bar-width: 2px;--lk-va-bar-gap: 4px;--lk-va-bar-border-radius: 1px}.lk-agent-control-bar .lk-audio-bar-visualizer .lk-audio-bar.lk-highlighted{filter:none}
2
3/*# sourceMappingURL=control-bar.css.map */
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/prefabs/control-bar.css.map view/app/src/vendor/livekit/styles/dist/general/prefabs/control-bar.css.map +1 −0 View view/app/src/vendor/livekit/styles/dist/general/prefabs/control-bar.css.map
@@ -0,0 +1 @@
1{"version":3,"sources":["../../../scss/prefabs/control-bar.scss"],"names":[],"mappings":"AAAA,sCACE,YAAA,CACA,SAAA,CACA,kBAAA,CACA,sBAAA,CACA,cAAA,CACA,2CAAA,CACA,uCAAA,CAGF,sBAGE,mCAAA,CACA,oBAAA,CACA,sBAAA,CACA,oBAAA,CACA,8BAAA,CAEE,4EACE,WAAA","file":"control-bar.css"}
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/prefabs/index.css view/app/src/vendor/livekit/styles/dist/general/prefabs/index.css +3 −0 View view/app/src/vendor/livekit/styles/dist/general/prefabs/index.css
@@ -0,0 +1,3 @@
1.lk-audio-conference{position:relative;width:100%;height:100%}.lk-audio-conference-stage{width:100%;height:100%;display:grid;grid-template-columns:repeat(3, 1fr);gap:10px}.lk-chat{display:grid;grid-template-rows:var(--lk-chat-header-height) 1fr var(--lk-control-bar-height);width:clamp(200px,55ch,60ch);background-color:var(--lk-bg2);border-left:1px solid var(--lk-border-color);align-items:end}.lk-chat-header{height:var(--lk-chat-header-height);padding:.75rem;position:relative;display:flex;align-items:center;justify-content:center}.lk-chat-header .lk-close-button{position:absolute;right:0;transform:translateX(-50%);background-color:rgba(0,0,0,0)}.lk-chat-header .lk-close-button:hover{background-color:var(--lk-control-active-hover-bg)}.lk-chat-messages{display:flex;width:100%;max-height:100%;flex-direction:column;gap:.25rem;overflow:auto}.lk-chat-entry{display:flex;flex-direction:column;gap:.25rem;margin:0 .25rem}.lk-chat-entry .lk-meta-data{font-size:.75rem;color:var(--lk-fg5);white-space:nowrap;padding:0 .3rem;display:flex}.lk-chat-entry .lk-meta-data .lk-participant-name{margin-top:1rem}.lk-chat-entry .lk-meta-data .lk-timestamp{margin-left:auto;align-self:flex-end}.lk-chat-entry .lk-edit-button{background:none;float:right;margin:0;padding:0 .25rem;border-radius:0;font-size:12px}.lk-chat-entry .lk-message-body{display:inline-block;border-radius:15px;padding:.25rem .75rem;word-break:break-word;width:fit-content;max-width:calc(100% - 32px)}.lk-chat-entry[data-lk-message-origin=local] .lk-message-body{background-color:var(--lk-bg5)}.lk-chat-entry[data-lk-message-origin=remote] .lk-message-body{background-color:var(--lk-accent4)}.lk-chat-entry a{text-decoration:underline;color:inherit}.lk-chat-entry *{margin-block-start:.25em;margin-block-end:.25em}.lk-chat-entry:last-child{margin-bottom:.25rem}.lk-chat-form{display:flex;gap:.75rem;padding:.75rem;border-top:1px solid var(--lk-border-color);max-height:var(--lk-control-bar-height)}.lk-chat-form-input{font-size:inherit;line-height:inherit;width:100%}@media(max-width: 600px){.lk-chat{position:fixed;top:0;right:0;max-width:100%;bottom:var(--lk-control-bar-height)}}.lk-control-bar,.lk-agent-control-bar{display:flex;gap:.5rem;align-items:center;justify-content:center;padding:.75rem;border-top:1px solid var(--lk-border-color);max-height:var(--lk-control-bar-height)}.lk-agent-control-bar{height:var(--lk-control-bar-height);--lk-bg: transparent;--lk-va-bar-width: 2px;--lk-va-bar-gap: 4px;--lk-va-bar-border-radius: 1px}.lk-agent-control-bar .lk-audio-bar-visualizer .lk-audio-bar.lk-highlighted{filter:none}.lk-prejoin{background-color:var(--lk-bg);line-height:var(--lk-line-height)}[data-lk-theme]{font-size:var(--lk-font-size);font-family:var(--lk-font-family);color:var(--lk-fg)}[data-lk-theme] .lk-list{list-style:none;margin:0;padding:0}[data-lk-theme] .lk-form-control{font-family:var(--lk-font-family);padding:.625rem 1rem;background-color:var(--lk-control-bg);border:1px solid var(--lk-border-color);border-radius:var(--lk-border-radius)}.lk-prejoin{box-sizing:border-box;display:flex;flex-direction:column;align-items:center;padding:1rem;gap:1rem;margin-inline:auto;background-color:var(--lk-bg);width:min(100%,480px);align-items:stretch}.lk-prejoin .lk-video-container{position:relative;width:100%;height:auto;aspect-ratio:16/10;background-color:#000;border-radius:var(--lk-border-radius);overflow:hidden}.lk-prejoin .lk-video-container video,.lk-prejoin .lk-video-container .lk-camera-off-note{display:block;width:100%;height:100%;object-fit:cover}.lk-prejoin .lk-video-container video[data-lk-facing-mode=user]{transform:rotateY(180deg)}.lk-prejoin .lk-video-container .lk-camera-off-note{position:absolute;top:0px;left:0px;width:100%;aspect-ratio:16/10;background-color:#000;display:grid;place-items:center}.lk-prejoin .lk-video-container .lk-camera-off-note>*{height:70%;max-width:100%}.lk-prejoin .lk-audio-container{display:none}.lk-prejoin .lk-audio-container audio{width:100%;height:auto}.lk-prejoin .lk-button-group-container{display:flex;flex-wrap:nowrap;gap:1rem}.lk-prejoin .lk-button-group-container>.lk-button-group{width:50%}.lk-prejoin .lk-button-group-container>.lk-button-group>.lk-button{justify-content:left}.lk-prejoin .lk-button-group-container>.lk-button-group>.lk-button:first-child{width:100%}@media(max-width: 400px){.lk-prejoin .lk-button-group-container{flex-wrap:wrap}.lk-prejoin .lk-button-group-container>.lk-button-group{width:100%}}.lk-prejoin .lk-username-container{display:flex;flex-direction:column;gap:1rem;width:100%;max-width:100%}.lk-prejoin .lk-join-button{--lk-control-fg: var(--lk-accent-fg);--lk-control-bg: var(--lk-accent-bg);--lk-control-hover-bg: var(--lk-accent2);--lk-control-active-bg: var(--lk-accent3);--lk-control-active-hover-bg: var(--lk-accent4);background-color:var(--lk-control-bg)}.lk-prejoin .lk-join-button:hover{background-color:var(--lk-control-hover-bg)}.lk-focus-layout-wrapper,.lk-grid-layout-wrapper{position:relative;display:flex;justify-content:center;width:100%;height:calc(100% - var(--lk-control-bar-height))}.lk-grid-layout-wrapper{flex-direction:column;align-items:center}.lk-focus-layout-wrapper{align-items:stretch}.lk-video-conference{position:relative;display:flex;align-items:stretch;height:100%}.lk-video-conference-inner{display:flex;flex-direction:column;align-items:stretch;width:100%}.lk-settings-menu-modal{position:fixed;top:50%;left:50%;transform:translate(-50%, -50%);background:var(--lk-bg);padding:1rem;border-radius:var(--lk-border-radius);display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:.75rem 1.25rem;background-color:var(--lk-bg);border:1px solid var(--lk-border-color);border-radius:var(--lk-border-radius);box-shadow:var(--lk-box-shadow);min-width:50vw;min-height:50vh;max-width:100%;max-height:100%;overflow-y:auto}
2
3/*# sourceMappingURL=index.css.map */
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/prefabs/index.css.map view/app/src/vendor/livekit/styles/dist/general/prefabs/index.css.map +1 −0 View view/app/src/vendor/livekit/styles/dist/general/prefabs/index.css.map
@@ -0,0 +1 @@
1{"version":3,"sources":["../../../scss/prefabs/audio-conference.scss","../../../scss/prefabs/chat.scss","../../../scss/prefabs/control-bar.scss","../../../scss/_preflight.scss","../../../scss/prefabs/prejoin.scss","../../../scss/prefabs/video-conference.scss"],"names":[],"mappings":"AAAA,qBACE,iBAAA,CACA,UAAA,CACA,WAAA,CAGF,2BACE,UAAA,CACA,WAAA,CACA,YAAA,CACA,oCAAA,CACA,QAAA,CCXF,SACE,YAAA,CACA,gFAAA,CACA,4BAAA,CACA,8BAAA,CACA,4CAAA,CACA,eAAA,CAGF,gBACE,mCAAA,CACA,cAAA,CACA,iBAAA,CACA,YAAA,CACA,kBAAA,CACA,sBAAA,CACA,iCACE,iBAAA,CACA,OAAA,CACA,0BAAA,CACA,8BAAA,CACA,uCACE,kDAAA,CAKN,kBACE,YAAA,CACA,UAAA,CACA,eAAA,CACA,qBAAA,CACA,UAAA,CACA,aAAA,CAGF,eACE,YAAA,CACA,qBAAA,CACA,UAAA,CACA,eAAA,CAEA,6BACE,gBAAA,CACA,mBAAA,CACA,kBAAA,CACA,eAAA,CACA,YAAA,CAEA,kDACE,eAAA,CAGF,2CACE,gBAAA,CACA,mBAAA,CAIJ,+BACE,eAAA,CACA,WAAA,CACA,QAAA,CACA,gBAAA,CACA,eAAA,CACA,cAAA,CAGF,gCACE,oBAAA,CACA,kBAAA,CACA,qBAAA,CACA,qBAAA,CACA,iBAAA,CACA,2BAAA,CAIA,8DACE,8BAAA,CAKF,+DACE,kCAAA,CAIJ,iBACE,yBAAA,CACA,aAAA,CAGF,iBACE,wBAAA,CACA,sBAAA,CAGF,0BACE,oBAAA,CAIJ,cACE,YAAA,CACA,UAAA,CACA,cAAA,CACA,2CAAA,CACA,uCAAA,CAGF,oBACE,iBAAA,CACA,mBAAA,CACA,UAAA,CAGF,yBACE,SACE,cAAA,CACA,KAAA,CACA,OAAA,CACA,cAAA,CACA,mCAAA,CAAA,CC5HJ,sCACE,YAAA,CACA,SAAA,CACA,kBAAA,CACA,sBAAA,CACA,cAAA,CACA,2CAAA,CACA,uCAAA,CAGF,sBAGE,mCAAA,CACA,oBAAA,CACA,sBAAA,CACA,oBAAA,CACA,8BAAA,CAEE,4EACE,WAAA,CCpBN,YACE,6BAAA,CACA,iCAAA,CAGF,gBACE,6BAAA,CACA,iCAAA,CACA,kBAAA,CAEA,yBACE,eAAA,CACA,QAAA,CACA,SAAA,CAGF,iCACE,iCAAA,CACA,oBAAA,CACA,qCAAA,CACA,uCAAA,CACA,qCAAA,CCnBJ,YAEE,qBAAA,CACA,YAAA,CACA,qBAAA,CACA,kBAAA,CACA,YAAA,CACA,QAAA,CACA,kBAAA,CACA,6BAAA,CACA,qBAAA,CACA,mBAAA,CAEA,gCACE,iBAAA,CACA,UAAA,CACA,WAAA,CACA,kBAAA,CACA,qBAAA,CACA,qCAAA,CACA,eAAA,CACA,0FAEE,aAAA,CACA,UAAA,CACA,WAAA,CACA,gBAAA,CAGF,gEACE,yBAAA,CAGF,oDACE,iBAAA,CACA,OAAA,CACA,QAAA,CACA,UAAA,CACA,kBAAA,CACA,qBAAA,CAEA,YAAA,CACA,kBAAA,CACA,sDACE,UAAA,CACA,cAAA,CAKN,gCACE,YAAA,CACA,sCACE,UAAA,CACA,WAAA,CAIJ,uCACE,YAAA,CACA,gBAAA,CACA,QAAA,CAEA,wDACE,SAAA,CAEA,mEACE,oBAAA,CAGF,+EACE,UAAA,CAIN,yBACE,uCACE,cAAA,CACA,wDACE,UAAA,CAAA,CAKN,mCACE,YAAA,CACA,qBAAA,CACA,QAAA,CACA,UAAA,CACA,cAAA,CAGF,4BACE,oCAAA,CACA,oCAAA,CACA,wCAAA,CACA,yCAAA,CACA,+CAAA,CACA,qCAAA,CACA,kCACE,2CAAA,CCtGN,iDAEE,iBAAA,CACA,YAAA,CACA,sBAAA,CACA,UAAA,CACA,gDAAA,CAGF,wBACE,qBAAA,CACA,kBAAA,CAGF,yBACE,mBAAA,CAGF,qBACE,iBAAA,CACA,YAAA,CACA,mBAAA,CACA,WAAA,CAGF,2BACE,YAAA,CACA,qBAAA,CACA,mBAAA,CACA,UAAA,CAGF,wBACE,cAAA,CACA,OAAA,CACA,QAAA,CACA,+BAAA,CACA,uBAAA,CACA,YAAA,CACA,qCAAA,CAEA,YAAA,CACA,qBAAA,CACA,kBAAA,CACA,SAAA,CACA,sBAAA,CACA,6BAAA,CACA,uCAAA,CACA,qCAAA,CACA,+BAAA,CACA,cAAA,CACA,eAAA,CACA,cAAA,CACA,eAAA,CACA,eAAA","file":"index.css"}
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/prefabs/prejoin.css view/app/src/vendor/livekit/styles/dist/general/prefabs/prejoin.css +3 −0 View view/app/src/vendor/livekit/styles/dist/general/prefabs/prejoin.css
@@ -0,0 +1,3 @@
1.lk-prejoin{background-color:var(--lk-bg);line-height:var(--lk-line-height)}[data-lk-theme]{font-size:var(--lk-font-size);font-family:var(--lk-font-family);color:var(--lk-fg)}[data-lk-theme] .lk-list{list-style:none;margin:0;padding:0}[data-lk-theme] .lk-form-control{font-family:var(--lk-font-family);padding:.625rem 1rem;background-color:var(--lk-control-bg);border:1px solid var(--lk-border-color);border-radius:var(--lk-border-radius)}.lk-prejoin{box-sizing:border-box;display:flex;flex-direction:column;align-items:center;padding:1rem;gap:1rem;margin-inline:auto;background-color:var(--lk-bg);width:min(100%,480px);align-items:stretch}.lk-prejoin .lk-video-container{position:relative;width:100%;height:auto;aspect-ratio:16/10;background-color:#000;border-radius:var(--lk-border-radius);overflow:hidden}.lk-prejoin .lk-video-container video,.lk-prejoin .lk-video-container .lk-camera-off-note{display:block;width:100%;height:100%;object-fit:cover}.lk-prejoin .lk-video-container video[data-lk-facing-mode=user]{transform:rotateY(180deg)}.lk-prejoin .lk-video-container .lk-camera-off-note{position:absolute;top:0px;left:0px;width:100%;aspect-ratio:16/10;background-color:#000;display:grid;place-items:center}.lk-prejoin .lk-video-container .lk-camera-off-note>*{height:70%;max-width:100%}.lk-prejoin .lk-audio-container{display:none}.lk-prejoin .lk-audio-container audio{width:100%;height:auto}.lk-prejoin .lk-button-group-container{display:flex;flex-wrap:nowrap;gap:1rem}.lk-prejoin .lk-button-group-container>.lk-button-group{width:50%}.lk-prejoin .lk-button-group-container>.lk-button-group>.lk-button{justify-content:left}.lk-prejoin .lk-button-group-container>.lk-button-group>.lk-button:first-child{width:100%}@media(max-width: 400px){.lk-prejoin .lk-button-group-container{flex-wrap:wrap}.lk-prejoin .lk-button-group-container>.lk-button-group{width:100%}}.lk-prejoin .lk-username-container{display:flex;flex-direction:column;gap:1rem;width:100%;max-width:100%}.lk-prejoin .lk-join-button{--lk-control-fg: var(--lk-accent-fg);--lk-control-bg: var(--lk-accent-bg);--lk-control-hover-bg: var(--lk-accent2);--lk-control-active-bg: var(--lk-accent3);--lk-control-active-hover-bg: var(--lk-accent4);background-color:var(--lk-control-bg)}.lk-prejoin .lk-join-button:hover{background-color:var(--lk-control-hover-bg)}
2
3/*# sourceMappingURL=prejoin.css.map */
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/prefabs/prejoin.css.map view/app/src/vendor/livekit/styles/dist/general/prefabs/prejoin.css.map +1 −0 View view/app/src/vendor/livekit/styles/dist/general/prefabs/prejoin.css.map
@@ -0,0 +1 @@
1{"version":3,"sources":["../../../scss/_preflight.scss","../../../scss/prefabs/prejoin.scss"],"names":[],"mappings":"AAAA,YACE,6BAAA,CACA,iCAAA,CAGF,gBACE,6BAAA,CACA,iCAAA,CACA,kBAAA,CAEA,yBACE,eAAA,CACA,QAAA,CACA,SAAA,CAGF,iCACE,iCAAA,CACA,oBAAA,CACA,qCAAA,CACA,uCAAA,CACA,qCAAA,CCnBJ,YAEE,qBAAA,CACA,YAAA,CACA,qBAAA,CACA,kBAAA,CACA,YAAA,CACA,QAAA,CACA,kBAAA,CACA,6BAAA,CACA,qBAAA,CACA,mBAAA,CAEA,gCACE,iBAAA,CACA,UAAA,CACA,WAAA,CACA,kBAAA,CACA,qBAAA,CACA,qCAAA,CACA,eAAA,CACA,0FAEE,aAAA,CACA,UAAA,CACA,WAAA,CACA,gBAAA,CAGF,gEACE,yBAAA,CAGF,oDACE,iBAAA,CACA,OAAA,CACA,QAAA,CACA,UAAA,CACA,kBAAA,CACA,qBAAA,CAEA,YAAA,CACA,kBAAA,CACA,sDACE,UAAA,CACA,cAAA,CAKN,gCACE,YAAA,CACA,sCACE,UAAA,CACA,WAAA,CAIJ,uCACE,YAAA,CACA,gBAAA,CACA,QAAA,CAEA,wDACE,SAAA,CAEA,mEACE,oBAAA,CAGF,+EACE,UAAA,CAIN,yBACE,uCACE,cAAA,CACA,wDACE,UAAA,CAAA,CAKN,mCACE,YAAA,CACA,qBAAA,CACA,QAAA,CACA,UAAA,CACA,cAAA,CAGF,4BACE,oCAAA,CACA,oCAAA,CACA,wCAAA,CACA,yCAAA,CACA,+CAAA,CACA,qCAAA,CACA,kCACE,2CAAA","file":"prejoin.css"}
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/prefabs/video-conference.css view/app/src/vendor/livekit/styles/dist/general/prefabs/video-conference.css +3 −0 View view/app/src/vendor/livekit/styles/dist/general/prefabs/video-conference.css
@@ -0,0 +1,3 @@
1.lk-focus-layout-wrapper,.lk-grid-layout-wrapper{position:relative;display:flex;justify-content:center;width:100%;height:calc(100% - var(--lk-control-bar-height))}.lk-grid-layout-wrapper{flex-direction:column;align-items:center}.lk-focus-layout-wrapper{align-items:stretch}.lk-video-conference{position:relative;display:flex;align-items:stretch;height:100%}.lk-video-conference-inner{display:flex;flex-direction:column;align-items:stretch;width:100%}.lk-settings-menu-modal{position:fixed;top:50%;left:50%;transform:translate(-50%, -50%);background:var(--lk-bg);padding:1rem;border-radius:var(--lk-border-radius);display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:.75rem 1.25rem;background-color:var(--lk-bg);border:1px solid var(--lk-border-color);border-radius:var(--lk-border-radius);box-shadow:var(--lk-box-shadow);min-width:50vw;min-height:50vh;max-width:100%;max-height:100%;overflow-y:auto}
2
3/*# sourceMappingURL=video-conference.css.map */
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/prefabs/video-conference.css.map view/app/src/vendor/livekit/styles/dist/general/prefabs/video-conference.css.map +1 −0 View view/app/src/vendor/livekit/styles/dist/general/prefabs/video-conference.css.map
@@ -0,0 +1 @@
1{"version":3,"sources":["../../../scss/prefabs/video-conference.scss"],"names":[],"mappings":"AAAA,iDAEE,iBAAA,CACA,YAAA,CACA,sBAAA,CACA,UAAA,CACA,gDAAA,CAGF,wBACE,qBAAA,CACA,kBAAA,CAGF,yBACE,mBAAA,CAGF,qBACE,iBAAA,CACA,YAAA,CACA,mBAAA,CACA,WAAA,CAGF,2BACE,YAAA,CACA,qBAAA,CACA,mBAAA,CACA,UAAA,CAGF,wBACE,cAAA,CACA,OAAA,CACA,QAAA,CACA,+BAAA,CACA,uBAAA,CACA,YAAA,CACA,qCAAA,CAEA,YAAA,CACA,qBAAA,CACA,kBAAA,CACA,SAAA,CACA,sBAAA,CACA,6BAAA,CACA,uCAAA,CACA,qCAAA,CACA,+BAAA,CACA,cAAA,CACA,eAAA,CACA,cAAA,CACA,eAAA,CACA,eAAA","file":"video-conference.css"}
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/themes/default.css view/app/src/vendor/livekit/styles/dist/general/themes/default.css +3 −0 View view/app/src/vendor/livekit/styles/dist/general/themes/default.css
@@ -0,0 +1,3 @@
1[data-lk-theme=default]{color-scheme:dark;--lk-bg: #111;--lk-bg2: rgb(29.75, 29.75, 29.75);--lk-bg3: rgb(42.5, 42.5, 42.5);--lk-bg4: rgb(55.25, 55.25, 55.25);--lk-bg5: #444444;--lk-fg: #fff;--lk-fg2: rgb(244.8, 244.8, 244.8);--lk-fg3: rgb(234.6, 234.6, 234.6);--lk-fg4: rgb(224.4, 224.4, 224.4);--lk-fg5: rgb(214.2, 214.2, 214.2);--lk-border-color: rgba(255, 255, 255, 0.1);--lk-accent-fg: #fff;--lk-accent-bg: #1f8cf9;--lk-accent2: rgb(50.867826087, 150.2, 249.532173913);--lk-accent3: rgb(70.7356521739, 160.4, 250.0643478261);--lk-accent4: rgb(90.6034782609, 170.6, 250.5965217391);--lk-danger-fg: #fff;--lk-danger: #f91f31;--lk-danger2: rgb(249.532173913, 50.867826087, 67.2713043478);--lk-danger3: rgb(250.0643478261, 70.7356521739, 85.5426086957);--lk-danger4: rgb(250.5965217391, 90.6034782609, 103.8139130435);--lk-success-fg: #fff;--lk-success: #1ff968;--lk-success2: rgb(50.867826087, 249.532173913, 117.3930434783);--lk-success3: rgb(70.7356521739, 250.0643478261, 130.7860869565);--lk-success4: rgb(90.6034782609, 250.5965217391, 144.1791304348);--lk-control-fg: var(--lk-fg);--lk-control-bg: var(--lk-bg2);--lk-control-hover-bg: var(--lk-bg3);--lk-control-active-bg: var(--lk-bg4);--lk-control-active-hover-bg: var(--lk-bg5);--lk-connection-excellent: #06db4d;--lk-connection-good: #f9b11f;--lk-connection-poor: #f91f31;--lk-font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";--lk-font-size: 16px;--lk-line-height: 1.5;--lk-border-radius: 0.5rem;--lk-box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.15);--lk-drop-shadow: rgba(255, 255, 255, 0.2) 0px 0px 24px;--lk-grid-gap: 0.5rem;--lk-control-bar-height: 69px;--lk-chat-header-height: 69px}
2
3/*# sourceMappingURL=default.css.map */
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/themes/default.css.map view/app/src/vendor/livekit/styles/dist/general/themes/default.css.map +1 −0 View view/app/src/vendor/livekit/styles/dist/general/themes/default.css.map
@@ -0,0 +1 @@
1{"version":3,"sources":["../../../scss/themes/default.scss"],"names":[],"mappings":"AASA,wBACE,iBAAA,CAEA,aAAA,CACA,kCAAA,CACA,+BAAA,CACA,kCAAA,CACA,iBAAA,CAEA,aAAA,CACA,kCAAA,CACA,kCAAA,CACA,kCAAA,CACA,kCAAA,CAEA,2CAAA,CAEA,oBAAA,CACA,uBAAA,CACA,qDAAA,CACA,uDAAA,CACA,uDAAA,CAEA,oBAAA,CACA,oBAAA,CACA,6DAAA,CACA,+DAAA,CACA,gEAAA,CAEA,qBAAA,CACA,qBAAA,CACA,+DAAA,CACA,iEAAA,CACA,iEAAA,CAEA,6BAAA,CACA,8BAAA,CACA,oCAAA,CACA,qCAAA,CACA,2CAAA,CAEA,kCAAA,CACA,6BAAA,CACA,6BAAA,CAEA,mIAAA,CAEA,oBAAA,CACA,qBAAA,CACA,0BAAA,CACA,oDAAA,CACA,uDAAA,CAEA,qBAAA,CACA,6BAAA,CACA,6BAAA","file":"default.css"}
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/themes/huddle.css view/app/src/vendor/livekit/styles/dist/general/themes/huddle.css +3 −0 View view/app/src/vendor/livekit/styles/dist/general/themes/huddle.css
@@ -0,0 +1,3 @@
1[data-lk-theme=huddle]{--lk-button-size: 3.125rem;--lk-primary-color: #e23067;--lk-primary-bg: whitesmoke;--lk-secondary-color: lightgrey}[data-lk-theme=huddle] .lk-button{border:0px;box-shadow:0 0 1px rgba(29,28,29,.13),0 1px 3px 0 rgba(0,0,0,.08);background-position:center;background-repeat:no-repeat;background-size:60%;background-color:var(--lk-primary-bg);cursor:pointer}[data-lk-theme=huddle] .lk-button[aria-pressed=false]{background-color:var(--lk-primary-bg)}[data-lk-theme=huddle] .lk-button[data-lk-source]{height:var(--lk-button-size);width:var(--lk-button-size);aspect-ratio:1/1;border-radius:99999px}[data-lk-theme=huddle] .lk-disconnect-button{width:calc(1.5*var(--lk-button-size));height:var(--lk-button-size)}[data-lk-theme=huddle] .lk-button[data-lk-source=microphone]{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='192' height='192' fill='%23000000' viewBox='0 0 256 256'%3E%3Crect width='256' height='256' fill='none'%3E%3C/rect%3E%3Cpath d='M176.4,181.3A72,72,0,0,1,56.4,136' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/path%3E%3Cpath d='M154.9,157.6A39.6,39.6,0,0,1,128,168h0a40,40,0,0,1-40-40V84' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/path%3E%3Cline x1='128' y1='200' x2='128' y2='232' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/line%3E%3Cline x1='48' y1='40' x2='208' y2='216' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/line%3E%3Cpath d='M94,43a39.8,39.8,0,0,1,34-19h0a40,40,0,0,1,40,40v60.4' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/path%3E%3Cpath d='M199.6,136a72.4,72.4,0,0,1-4.5,18.2' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/path%3E%3C/svg%3E")}[data-lk-theme=huddle] .lk-button[data-lk-source=microphone][aria-pressed=true]{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='192' height='192' fill='%23000000' viewBox='0 0 256 256'%3E%3Crect width='256' height='256' fill='none'%3E%3C/rect%3E%3Crect x='88' y='24' width='80' height='144' rx='40' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/rect%3E%3Cline x1='128' y1='200' x2='128' y2='232' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/line%3E%3Cpath d='M199.6,136a72.1,72.1,0,0,1-143.2,0' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/path%3E%3C/svg%3E")}[data-lk-theme=huddle] .lk-button[data-lk-source=camera]{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='192' height='192' fill='%23000000' viewBox='0 0 256 256'%3E%3Crect width='256' height='256' fill='none'%3E%3C/rect%3E%3Cpolygon points='240 176 184 144 184 112 240 80 240 176' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/polygon%3E%3Cline x1='34.9' y1='24' x2='224' y2='232' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/line%3E%3Cpath d='M110.9,60H176a8,8,0,0,1,8,8v76' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/path%3E%3Cpath d='M184,188a8,8,0,0,1-8,8H24a8,8,0,0,1-8-8V68a8,8,0,0,1,8-8H67.6' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/path%3E%3C/svg%3E")}[data-lk-theme=huddle] .lk-button[data-lk-source=camera][aria-pressed=true]{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='192' height='192' fill='%23000000' viewBox='0 0 256 256'%3E%3Crect width='256' height='256' fill='none'%3E%3C/rect%3E%3Cpath d='M24,60H152a32,32,0,0,1,32,32v96a8,8,0,0,1-8,8H48a32,32,0,0,1-32-32V68A8,8,0,0,1,24,60Z' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/path%3E%3Cpolyline points='184 112 240 80 240 176 184 144' fill='none' stroke='%23000000' stroke-linecap='round' stroke-linejoin='round' stroke-width='16'%3E%3C/polyline%3E%3C/svg%3E")}[data-lk-theme=huddle] .lk-join-button{background-color:var(--lk-primary-color)}
2
3/*# sourceMappingURL=huddle.css.map */
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/themes/huddle.css.map view/app/src/vendor/livekit/styles/dist/general/themes/huddle.css.map +1 −0 View view/app/src/vendor/livekit/styles/dist/general/themes/huddle.css.map
@@ -0,0 +1 @@
1{"version":3,"sources":["../../../scss/themes/huddle.scss"],"names":[],"mappings":"AAAA,uBACE,0BAAA,CACA,2BAAA,CACA,2BAAA,CACA,+BAAA,CAEA,kCACE,UAAA,CACA,iEAAA,CACA,0BAAA,CACA,2BAAA,CACA,mBAAA,CACA,qCAAA,CACA,cAAA,CAEA,sDACE,qCAAA,CAIJ,kDACE,4BAAA,CACA,2BAAA,CACA,gBAAA,CACA,qBAAA,CAGF,6CACE,qCAAA,CACA,4BAAA,CAGF,6DACE,uqCAAA,CACA,gFACE,mrBAAA,CAIJ,yDACE,82BAAA,CACA,4EACE,glBAAA,CAIJ,uCACE,wCAAA","file":"huddle.css"}
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/index.css view/app/src/vendor/livekit/styles/dist/general/index.css +3 −0 View view/app/src/vendor/livekit/styles/dist/general/index.css
@@ -0,0 +1,3 @@
1[data-lk-theme]{font-size:var(--lk-font-size);font-family:var(--lk-font-family);color:var(--lk-fg)}[data-lk-theme] .lk-list{list-style:none;margin:0;padding:0}[data-lk-theme] .lk-form-control{font-family:var(--lk-font-family);padding:.625rem 1rem;background-color:var(--lk-control-bg);border:1px solid var(--lk-border-color);border-radius:var(--lk-border-radius)}[data-lk-theme=default]{color-scheme:dark;--lk-bg: #111;--lk-bg2: rgb(29.75, 29.75, 29.75);--lk-bg3: rgb(42.5, 42.5, 42.5);--lk-bg4: rgb(55.25, 55.25, 55.25);--lk-bg5: #444444;--lk-fg: #fff;--lk-fg2: rgb(244.8, 244.8, 244.8);--lk-fg3: rgb(234.6, 234.6, 234.6);--lk-fg4: rgb(224.4, 224.4, 224.4);--lk-fg5: rgb(214.2, 214.2, 214.2);--lk-border-color: rgba(255, 255, 255, 0.1);--lk-accent-fg: #fff;--lk-accent-bg: #1f8cf9;--lk-accent2: rgb(50.867826087, 150.2, 249.532173913);--lk-accent3: rgb(70.7356521739, 160.4, 250.0643478261);--lk-accent4: rgb(90.6034782609, 170.6, 250.5965217391);--lk-danger-fg: #fff;--lk-danger: #f91f31;--lk-danger2: rgb(249.532173913, 50.867826087, 67.2713043478);--lk-danger3: rgb(250.0643478261, 70.7356521739, 85.5426086957);--lk-danger4: rgb(250.5965217391, 90.6034782609, 103.8139130435);--lk-success-fg: #fff;--lk-success: #1ff968;--lk-success2: rgb(50.867826087, 249.532173913, 117.3930434783);--lk-success3: rgb(70.7356521739, 250.0643478261, 130.7860869565);--lk-success4: rgb(90.6034782609, 250.5965217391, 144.1791304348);--lk-control-fg: var(--lk-fg);--lk-control-bg: var(--lk-bg2);--lk-control-hover-bg: var(--lk-bg3);--lk-control-active-bg: var(--lk-bg4);--lk-control-active-hover-bg: var(--lk-bg5);--lk-connection-excellent: #06db4d;--lk-connection-good: #f9b11f;--lk-connection-poor: #f91f31;--lk-font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";--lk-font-size: 16px;--lk-line-height: 1.5;--lk-border-radius: 0.5rem;--lk-box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.15);--lk-drop-shadow: rgba(255, 255, 255, 0.2) 0px 0px 24px;--lk-grid-gap: 0.5rem;--lk-control-bar-height: 69px;--lk-chat-header-height: 69px}.lk-button,.lk-start-audio-button,.lk-chat-toggle,.lk-disconnect-button{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:.5rem;padding:.625rem 1rem;color:var(--lk-control-fg);background-image:none;background-color:var(--lk-control-bg);border:0;border-radius:var(--lk-border-radius);cursor:pointer;white-space:nowrap;font-size:inherit;line-height:inherit;user-select:none}.lk-button:not(:disabled):hover,.lk-start-audio-button:not(:disabled):hover,.lk-chat-toggle:not(:disabled):hover,.lk-disconnect-button:not(:disabled):hover{background-color:var(--lk-control-hover-bg)}.lk-button>svg,.lk-start-audio-button>svg,.lk-chat-toggle>svg,.lk-disconnect-button>svg{overflow:visible}.lk-button[aria-pressed=true],[aria-pressed=true].lk-start-audio-button,[aria-pressed=true].lk-chat-toggle,[aria-pressed=true].lk-disconnect-button{background-color:var(--lk-control-active-bg)}.lk-button[aria-pressed=true]:hover,[aria-pressed=true].lk-start-audio-button:hover,[aria-pressed=true].lk-chat-toggle:hover,[aria-pressed=true].lk-disconnect-button:hover{background-color:var(--lk-control-active-hover-bg)}.lk-button[data-lk-source=screen_share][data-lk-enabled=true],[data-lk-source=screen_share][data-lk-enabled=true].lk-start-audio-button,[data-lk-source=screen_share][data-lk-enabled=true].lk-chat-toggle,[data-lk-source=screen_share][data-lk-enabled=true].lk-disconnect-button{background-color:var(--lk-accent-bg)}.lk-button[data-lk-source=screen_share][data-lk-enabled=true]:hover,[data-lk-source=screen_share][data-lk-enabled=true].lk-start-audio-button:hover,[data-lk-source=screen_share][data-lk-enabled=true].lk-chat-toggle:hover,[data-lk-source=screen_share][data-lk-enabled=true].lk-disconnect-button:hover{background-color:var(--lk-accent2)}.lk-button:disabled,.lk-start-audio-button:disabled,.lk-chat-toggle:disabled,.lk-disconnect-button:disabled{opacity:.5}.lk-button-group{display:inline-flex;align-items:stretch;height:100%}.lk-button-group>.lk-button:first-child,.lk-button-group>.lk-start-audio-button:first-child,.lk-button-group>.lk-chat-toggle:first-child,.lk-button-group>.lk-disconnect-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.lk-button-group-menu{position:relative;flex-shrink:0}.lk-button-group-menu>.lk-button,.lk-button-group-menu>.lk-start-audio-button,.lk-button-group-menu>.lk-chat-toggle,.lk-button-group-menu>.lk-disconnect-button{height:100%;border-top-left-radius:0;border-bottom-left-radius:0}.lk-button-group-menu>.lk-button::after,.lk-button-group-menu>.lk-start-audio-button::after,.lk-button-group-menu>.lk-chat-toggle::after,.lk-button-group-menu>.lk-disconnect-button::after{margin-left:0}.lk-button-menu::after{display:inline-block;content:"";width:.5em;height:.5em;margin-top:-0.25rem;margin-left:.5rem;border-left:.125em solid;border-bottom:.125em solid;transform:rotate(-45deg);transform-origin:center center}.lk-disconnect-button{font-weight:600;color:var(--lk-danger);border:1px solid var(--lk-danger)}.lk-disconnect-button:not(:disabled):hover{--lk-control-hover-bg: var(--lk-danger2);color:var(--lk-danger-fg)}.lk-disconnect-button:not(:disabled):active{--lk-control-hover-bg: var(--lk-danger3);color:var(--lk-danger-fg)}.lk-chat-toggle{position:relative}.lk-chat-toggle[data-lk-unread-msgs]:not([data-lk-unread-msgs="0"]):after{content:attr(data-lk-unread-msgs);position:absolute;top:0;left:0;padding:.25rem;margin-left:.25rem;margin-top:.25rem;border-radius:50%;font-size:.5rem;line-height:.75;background:var(--lk-accent-bg)}.lk-media-device-select:not(:last-child){padding-bottom:.5rem;margin-bottom:.75rem;border-bottom:1px solid var(--lk-border-color)}.lk-media-device-select li:not(:last-child){margin-bottom:.25rem}.lk-media-device-select li>.lk-button{width:100%;justify-content:start;padding-block:.5rem}.lk-media-device-select li:not([data-lk-active=true])>.lk-button:not(:disabled):hover{background-color:var(--lk-bg3)}.lk-media-device-select [data-lk-active=false]>.lk-button:hover{cursor:pointer;background-color:rgba(0,0,0,.05)}.lk-media-device-select [data-lk-active=true]>.lk-button{color:var(--lk-accent-fg);background-color:var(--lk-accent-bg)}.lk-device-menu{width:max-content;position:absolute;top:0;left:0;z-index:5;min-width:10rem;padding:.5rem;margin-bottom:.25rem;white-space:nowrap;background-color:var(--lk-bg2);border:1px solid var(--lk-border-color);border-radius:.75rem;box-shadow:var(--lk-box-shadow)}.lk-device-menu-heading{padding:.25rem .5rem;font-weight:bold;opacity:.65}.lk-start-audio-button{color:var(--lk-accent-fg);background-color:var(--lk-accent-bg)}@media screen and (max-width: 600px){.lk-start-audio-button{position:fixed;top:50%;left:50%;transform:translate(-50%, -50%)}}.lk-pagination-control{position:absolute;bottom:1rem;left:50%;transform:translateX(-50%);display:flex;align-items:stretch;background-color:var(--lk-control-bg);border-radius:var(--lk-border-radius);transition:opacity ease-in-out .15s;opacity:0}.lk-pagination-control:hover{opacity:1}.lk-pagination-control>.lk-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.lk-pagination-control>.lk-button:first-child>svg{transform:rotate(180deg)}.lk-pagination-control>.lk-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.lk-pagination-count{padding:.5rem .875rem;border-inline:1px solid var(--lk-bg)}[data-lk-user-interaction=true].lk-pagination-control{opacity:1}.lk-pagination-indicator{position:absolute;height:var(--lk-grid-gap);background-color:var(--lk-bg2);width:fit-content;padding:.2rem .5rem;bottom:calc(var(--lk-grid-gap)/2);left:50%;transform:translateX(-50%);border-radius:2rem;opacity:1;display:flex;gap:.2rem;align-items:center}.lk-pagination-indicator span{display:inline-block;width:.4rem;height:.4rem;border-radius:9999999px;background-color:var(--lk-fg);opacity:.35;transition:opacity linear .2s}.lk-pagination-indicator span[data-lk-active]{opacity:.9}.lk-grid-layout{--lk-col-count: 1;--lk-row-count: 1;display:grid;grid-template-columns:repeat(var(--lk-col-count), minmax(0, 1fr));grid-auto-rows:minmax(0, 1fr);grid-gap:var(--lk-grid-gap);width:100%;height:100%;max-width:100%;max-height:100%;padding:var(--lk-grid-gap)}.lk-grid-layout[data-lk-pagination=true]{padding-bottom:calc(var(--lk-grid-gap)*2)}.lk-focus-layout{display:grid;grid-template-columns:1fr 5fr;gap:var(--lk-grid-gap);width:100%;max-height:100%;padding:var(--lk-grid-gap)}.lk-focused-participant{position:relative}.lk-focused-participant .lk-pip-track{position:absolute;top:10px;right:10px;width:20%;height:auto}@media(max-width: 600px){.lk-focus-layout{grid-template-columns:1fr;grid-template-rows:5fr 1fr}.lk-carousel{order:1}}.lk-carousel{max-height:100%;display:flex;gap:var(--lk-grid-gap)}.lk-carousel>*{flex-shrink:0;aspect-ratio:16/10;scroll-snap-align:start}.lk-carousel[data-lk-orientation=vertical]{flex-direction:column;scroll-snap-type:y mandatory;overflow-y:auto;overflow-x:hidden}.lk-carousel[data-lk-orientation=vertical]>*{--lk-height-minus-gaps: calc(100% - calc(var(--lk-grid-gap) * calc(var(--lk-max-visible-tiles) - 1)));height:calc(var(--lk-height-minus-gaps)/var(--lk-max-visible-tiles))}.lk-carousel[data-lk-orientation=horizontal]{scroll-snap-type:x mandatory;overflow-y:hidden;overflow-x:auto}.lk-carousel[data-lk-orientation=horizontal]>*{--lk-width-minus-gaps: calc(100% - var(--lk-grid-gap) * (var(--lk-max-visible-tiles) - 1));width:calc(var(--lk-width-minus-gaps)/var(--lk-max-visible-tiles))}.lk-connection-quality{width:1.5rem;height:1.5rem}.lk-track-muted-indicator-camera,.lk-track-muted-indicator-microphone{position:relative;width:var(--lk-indicator-size, 1rem);height:var(--lk-indicator-size, 1rem);margin-inline-end:.25rem;transition:opacity .25s ease-in-out}.lk-track-muted-indicator-camera[data-lk-muted=true]{opacity:.5}.lk-track-muted-indicator-microphone{--lk-bg: var(--lk-icon-mic)}.lk-track-muted-indicator-microphone[data-lk-muted=true]{opacity:.5}.lk-participant-name{font-size:.875rem}.lk-participant-media-video{width:100%;height:100%;object-fit:cover;object-position:center;background-color:#000}.lk-participant-media-video[data-lk-orientation=landscape]{object-fit:cover}.lk-participant-media-video[data-lk-orientation=portrait],.lk-participant-media-video[data-lk-source=screen_share]{object-fit:contain;background-color:var(--lk-bg2)}.lk-participant-media-audio{width:auto}[data-lk-facing-mode=user] .lk-participant-media-video[data-lk-local-participant=true][data-lk-source=camera]{transform:rotateY(180deg)}.lk-audio-visualizer{width:100%;height:100%;min-height:160px;background:var(--lk-bg-control);aspect-ratio:16/9;border-radius:.5rem;display:flex;justify-content:space-around;align-items:center}.lk-audio-visualizer>rect{fill:var(--lk-accent-bg);transition:transform 100ms cubic-bezier(0.19, 0.02, 0.09, 1)}.lk-audio-visualizer>path{stroke:var(--lk-accent-bg);transition:100ms cubic-bezier(0.19, 0.02, 0.09, 1)}.lk-audio-bar-visualizer{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:var(--lk-bg);gap:var(--lk-va-bar-gap, 24px)}.lk-audio-bar-visualizer>.lk-audio-bar{transform-origin:"center";height:100%;width:var(--lk-va-bar-width, 12px);border-radius:var(--lk-va-bar-border-radius, 32px);background-color:var(--lk-va-bar-bg, rgba(136, 136, 136, 0.2));transition:background-color .25s ease-out}.lk-audio-bar-visualizer[data-lk-va-state=speaking]>.lk-audio-bar,.lk-audio-bar-visualizer>.lk-audio-bar.lk-highlighted,.lk-audio-bar-visualizer>[data-lk-highlighted=true]{background-color:var(--lk-fg, rgb(136, 136, 136));transition:none}.lk-audio-bar-visualizer[data-lk-va-state=thinking]{transition:background-color .15s ease-out}.lk-participant-tile{--lk-speaking-indicator-width: 2.5px;position:relative;display:flex;flex-direction:column;gap:.375rem;overflow:hidden;border-radius:var(--lk-border-radius)}.lk-participant-tile::after{content:"";position:absolute;top:0;bottom:0;left:0;right:0;border-radius:var(--lk-border-radius);border:0px solid var(--lk-accent-bg);transition-property:border opacity;transition-delay:.5s;transition-duration:.4s;pointer-events:none}.lk-participant-tile[data-lk-speaking=true]:not([data-lk-source=screen_share])::after{transition-delay:0s;transition-duration:.2s;border-width:var(--lk-speaking-indicator-width)}.lk-participant-tile .lk-focus-toggle-button{position:absolute;top:.25rem;right:.25rem;padding:.25rem;background-color:rgba(0,0,0,.5);border-radius:calc(var(--lk-border-radius)/2);opacity:0;transition:opacity .2s ease-in-out;transition-delay:.2s}.lk-participant-tile:hover .lk-focus-toggle-button,.lk-participant-tile:focus .lk-focus-toggle-button{opacity:1;transition-delay:0}.lk-participant-tile .lk-connection-quality{opacity:0;transition:opacity .2s ease-in-out;transition-delay:.2s}.lk-participant-tile .lk-connection-quality[data-lk-quality=poor]{opacity:1;transition-delay:0}.lk-participant-tile:hover .lk-connection-quality,.lk-participant-tile:focus .lk-connection-quality{opacity:1;transition-delay:0}.lk-participant-tile .lk-participant-placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background-color:var(--lk-bg2);opacity:0;transition:opacity .2s ease-in-out;pointer-events:none;border-radius:var(--lk-border-radius)}.lk-participant-tile .lk-participant-placeholder svg{height:100%;width:auto;padding:10%}.lk-participant-tile[data-lk-video-muted=true][data-lk-source=camera] .lk-participant-placeholder{opacity:1}.lk-participant-metadata{position:absolute;right:.25rem;bottom:.25rem;left:.25rem;display:flex;flex-direction:row;align-items:center;justify-content:space-between;gap:.5rem;line-height:1}.lk-participant-metadata-item{display:flex;align-items:center;padding:.25rem;background-color:rgba(0,0,0,.5);border-radius:calc(var(--lk-border-radius)/2)}.lk-toast{position:fixed;top:.75rem;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:.5rem;padding:.75rem 1.25rem;background-color:var(--lk-bg);border:1px solid var(--lk-border-color);border-radius:var(--lk-border-radius);box-shadow:var(--lk-box-shadow)}.lk-spinner{animation:lk-rotate 2s infinite linear}@keyframes lk-rotate{from{transform:rotate(0deg)}to{transform:rotate(359deg)}}.lk-room-container{background-color:var(--lk-bg);line-height:var(--lk-line-height)}[data-lk-theme]{font-size:var(--lk-font-size);font-family:var(--lk-font-family);color:var(--lk-fg)}[data-lk-theme] .lk-list{list-style:none;margin:0;padding:0}[data-lk-theme] .lk-form-control{font-family:var(--lk-font-family);padding:.625rem 1rem;background-color:var(--lk-control-bg);border:1px solid var(--lk-border-color);border-radius:var(--lk-border-radius)}.lk-room-container{position:relative;width:100%;height:100%;--lk-has-imported-styles: "true"}.lk-room-container *[class^=lk-],.lk-room-container *[class*=" lk-"]{box-sizing:border-box}.lk-audio-conference{position:relative;width:100%;height:100%}.lk-audio-conference-stage{width:100%;height:100%;display:grid;grid-template-columns:repeat(3, 1fr);gap:10px}.lk-chat{display:grid;grid-template-rows:var(--lk-chat-header-height) 1fr var(--lk-control-bar-height);width:clamp(200px,55ch,60ch);background-color:var(--lk-bg2);border-left:1px solid var(--lk-border-color);align-items:end}.lk-chat-header{height:var(--lk-chat-header-height);padding:.75rem;position:relative;display:flex;align-items:center;justify-content:center}.lk-chat-header .lk-close-button{position:absolute;right:0;transform:translateX(-50%);background-color:rgba(0,0,0,0)}.lk-chat-header .lk-close-button:hover{background-color:var(--lk-control-active-hover-bg)}.lk-chat-messages{display:flex;width:100%;max-height:100%;flex-direction:column;gap:.25rem;overflow:auto}.lk-chat-entry{display:flex;flex-direction:column;gap:.25rem;margin:0 .25rem}.lk-chat-entry .lk-meta-data{font-size:.75rem;color:var(--lk-fg5);white-space:nowrap;padding:0 .3rem;display:flex}.lk-chat-entry .lk-meta-data .lk-participant-name{margin-top:1rem}.lk-chat-entry .lk-meta-data .lk-timestamp{margin-left:auto;align-self:flex-end}.lk-chat-entry .lk-edit-button{background:none;float:right;margin:0;padding:0 .25rem;border-radius:0;font-size:12px}.lk-chat-entry .lk-message-body{display:inline-block;border-radius:15px;padding:.25rem .75rem;word-break:break-word;width:fit-content;max-width:calc(100% - 32px)}.lk-chat-entry[data-lk-message-origin=local] .lk-message-body{background-color:var(--lk-bg5)}.lk-chat-entry[data-lk-message-origin=remote] .lk-message-body{background-color:var(--lk-accent4)}.lk-chat-entry a{text-decoration:underline;color:inherit}.lk-chat-entry *{margin-block-start:.25em;margin-block-end:.25em}.lk-chat-entry:last-child{margin-bottom:.25rem}.lk-chat-form{display:flex;gap:.75rem;padding:.75rem;border-top:1px solid var(--lk-border-color);max-height:var(--lk-control-bar-height)}.lk-chat-form-input{font-size:inherit;line-height:inherit;width:100%}@media(max-width: 600px){.lk-chat{position:fixed;top:0;right:0;max-width:100%;bottom:var(--lk-control-bar-height)}}.lk-control-bar,.lk-agent-control-bar{display:flex;gap:.5rem;align-items:center;justify-content:center;padding:.75rem;border-top:1px solid var(--lk-border-color);max-height:var(--lk-control-bar-height)}.lk-agent-control-bar{height:var(--lk-control-bar-height);--lk-bg: transparent;--lk-va-bar-width: 2px;--lk-va-bar-gap: 4px;--lk-va-bar-border-radius: 1px}.lk-agent-control-bar .lk-audio-bar-visualizer .lk-audio-bar.lk-highlighted{filter:none}.lk-prejoin{background-color:var(--lk-bg);line-height:var(--lk-line-height)}[data-lk-theme]{font-size:var(--lk-font-size);font-family:var(--lk-font-family);color:var(--lk-fg)}[data-lk-theme] .lk-list{list-style:none;margin:0;padding:0}[data-lk-theme] .lk-form-control{font-family:var(--lk-font-family);padding:.625rem 1rem;background-color:var(--lk-control-bg);border:1px solid var(--lk-border-color);border-radius:var(--lk-border-radius)}.lk-prejoin{box-sizing:border-box;display:flex;flex-direction:column;align-items:center;padding:1rem;gap:1rem;margin-inline:auto;background-color:var(--lk-bg);width:min(100%,480px);align-items:stretch}.lk-prejoin .lk-video-container{position:relative;width:100%;height:auto;aspect-ratio:16/10;background-color:#000;border-radius:var(--lk-border-radius);overflow:hidden}.lk-prejoin .lk-video-container video,.lk-prejoin .lk-video-container .lk-camera-off-note{display:block;width:100%;height:100%;object-fit:cover}.lk-prejoin .lk-video-container video[data-lk-facing-mode=user]{transform:rotateY(180deg)}.lk-prejoin .lk-video-container .lk-camera-off-note{position:absolute;top:0px;left:0px;width:100%;aspect-ratio:16/10;background-color:#000;display:grid;place-items:center}.lk-prejoin .lk-video-container .lk-camera-off-note>*{height:70%;max-width:100%}.lk-prejoin .lk-audio-container{display:none}.lk-prejoin .lk-audio-container audio{width:100%;height:auto}.lk-prejoin .lk-button-group-container{display:flex;flex-wrap:nowrap;gap:1rem}.lk-prejoin .lk-button-group-container>.lk-button-group{width:50%}.lk-prejoin .lk-button-group-container>.lk-button-group>.lk-button{justify-content:left}.lk-prejoin .lk-button-group-container>.lk-button-group>.lk-button:first-child{width:100%}@media(max-width: 400px){.lk-prejoin .lk-button-group-container{flex-wrap:wrap}.lk-prejoin .lk-button-group-container>.lk-button-group{width:100%}}.lk-prejoin .lk-username-container{display:flex;flex-direction:column;gap:1rem;width:100%;max-width:100%}.lk-prejoin .lk-join-button{--lk-control-fg: var(--lk-accent-fg);--lk-control-bg: var(--lk-accent-bg);--lk-control-hover-bg: var(--lk-accent2);--lk-control-active-bg: var(--lk-accent3);--lk-control-active-hover-bg: var(--lk-accent4);background-color:var(--lk-control-bg)}.lk-prejoin .lk-join-button:hover{background-color:var(--lk-control-hover-bg)}.lk-focus-layout-wrapper,.lk-grid-layout-wrapper{position:relative;display:flex;justify-content:center;width:100%;height:calc(100% - var(--lk-control-bar-height))}.lk-grid-layout-wrapper{flex-direction:column;align-items:center}.lk-focus-layout-wrapper{align-items:stretch}.lk-video-conference{position:relative;display:flex;align-items:stretch;height:100%}.lk-video-conference-inner{display:flex;flex-direction:column;align-items:stretch;width:100%}.lk-settings-menu-modal{position:fixed;top:50%;left:50%;transform:translate(-50%, -50%);background:var(--lk-bg);padding:1rem;border-radius:var(--lk-border-radius);display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:.75rem 1.25rem;background-color:var(--lk-bg);border:1px solid var(--lk-border-color);border-radius:var(--lk-border-radius);box-shadow:var(--lk-box-shadow);min-width:50vw;min-height:50vh;max-width:100%;max-height:100%;overflow-y:auto}
2
3/*# sourceMappingURL=index.css.map */
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/general/index.css.map view/app/src/vendor/livekit/styles/dist/general/index.css.map +1 −0 View view/app/src/vendor/livekit/styles/dist/general/index.css.map
@@ -0,0 +1 @@
1{"version":3,"sources":["../../scss/_preflight.scss","../../scss/themes/default.scss","../../scss/components/controls/_button.scss","../../scss/components/controls/_disconnect-button.scss","../../scss/components/controls/_chat-toggle.scss","../../scss/components/controls/_media-device-select.scss","../../scss/components/controls/_start-audio.scss","../../scss/components/controls/_pagination-control.scss","../../scss/components/controls/_pagination-indicator.scss","../../scss/components/layout/_grid-layout.scss","../../scss/components/layout/_focus-layout.scss","../../scss/components/layout/_carousel-view.scss","../../scss/components/participant/_connection-quality.scss","../../scss/components/participant/_track-muted-indicator.scss","../../scss/components/participant/_participant-name.scss","../../scss/components/participant/_participant-media.scss","../../scss/components/participant/_audio-visualizer.scss","../../scss/components/participant/_participant-tile.scss","../../scss/components/_toast.scss","../../scss/components/_room.scss","../../scss/prefabs/audio-conference.scss","../../scss/prefabs/chat.scss","../../scss/prefabs/control-bar.scss","../../scss/prefabs/prejoin.scss","../../scss/prefabs/video-conference.scss"],"names":[],"mappings":"AAKA,gBACE,6BAAA,CACA,iCAAA,CACA,kBAAA,CAEA,yBACE,eAAA,CACA,QAAA,CACA,SAAA,CAGF,iCACE,iCAAA,CACA,oBAAA,CACA,qCAAA,CACA,uCAAA,CACA,qCAAA,CCZJ,wBACE,iBAAA,CAEA,aAAA,CACA,kCAAA,CACA,+BAAA,CACA,kCAAA,CACA,iBAAA,CAEA,aAAA,CACA,kCAAA,CACA,kCAAA,CACA,kCAAA,CACA,kCAAA,CAEA,2CAAA,CAEA,oBAAA,CACA,uBAAA,CACA,qDAAA,CACA,uDAAA,CACA,uDAAA,CAEA,oBAAA,CACA,oBAAA,CACA,6DAAA,CACA,+DAAA,CACA,gEAAA,CAEA,qBAAA,CACA,qBAAA,CACA,+DAAA,CACA,iEAAA,CACA,iEAAA,CAEA,6BAAA,CACA,8BAAA,CACA,oCAAA,CACA,qCAAA,CACA,2CAAA,CAEA,kCAAA,CACA,6BAAA,CACA,6BAAA,CAEA,mIAAA,CAEA,oBAAA,CACA,qBAAA,CACA,0BAAA,CACA,oDAAA,CACA,uDAAA,CAEA,qBAAA,CACA,6BAAA,CACA,6BAAA,CChEF,wEACE,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,sBAAA,CACA,SAAA,CACA,oBAAA,CACA,0BAAA,CACA,qBAAA,CACA,qCAAA,CACA,QAAA,CACA,qCAAA,CACA,cAAA,CACA,kBAAA,CACA,iBAAA,CACA,mBAAA,CACA,gBAAA,CAEA,4JACE,2CAAA,CAGF,wFACE,gBAAA,CAGF,oJACE,4CAAA,CAEA,4KACE,kDAAA,CAKJ,oRACE,oCAAA,CACA,4SACE,kCAAA,CAIJ,4GACE,UAAA,CAIJ,iBACE,mBAAA,CACA,mBAAA,CACA,WAAA,CAEA,4LACE,yBAAA,CACA,4BAAA,CAIJ,sBACE,iBAAA,CACA,aAAA,CAEA,gKACE,WAAA,CACA,wBAAA,CACA,2BAAA,CAEA,4LACE,aAAA,CAOJ,uBACE,oBAAA,CACA,UAAA,CACA,UAAA,CACA,WAAA,CACA,mBAAA,CACA,iBAAA,CACA,wBAAA,CACA,0BAAA,CACA,wBAAA,CACA,8BAAA,CCnFJ,sBAIE,eAAA,CACA,sBAAA,CACA,iCAAA,CAEA,2CACE,wCAAA,CACA,yBAAA,CAEF,4CACE,wCAAA,CACA,yBAAA,CCdJ,gBAEE,iBAAA,CAGF,0EACE,iCAAA,CACA,iBAAA,CACA,KAAA,CACA,MAAA,CACA,cAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,eAAA,CACA,eAAA,CACA,8BAAA,CCjBA,yCACE,oBAAA,CACA,oBAAA,CACA,8CAAA,CAIA,4CACE,oBAAA,CAGF,sCACE,UAAA,CACA,qBAAA,CACA,mBAAA,CAGF,sFACE,8BAAA,CAKF,gEACE,cAAA,CACA,gCAAA,CAKF,yDACE,yBAAA,CACA,oCAAA,CAKN,gBACE,iBAAA,CACA,iBAAA,CACA,KAAA,CACA,MAAA,CACA,SAAA,CACA,eAAA,CACA,aAAA,CACA,oBAAA,CACA,kBAAA,CACA,8BAAA,CACA,uCAAA,CACA,oBAAA,CACA,+BAAA,CAGF,wBACE,oBAAA,CACA,gBAAA,CACA,WAAA,CCvDF,uBAEE,yBAAA,CACA,oCAAA,CAEA,qCALF,uBAMI,cAAA,CACA,OAAA,CACA,QAAA,CACA,+BAAA,CAAA,CCXJ,uBACE,iBAAA,CACA,WAAA,CACA,QAAA,CACA,0BAAA,CACA,YAAA,CACA,mBAAA,CACA,qCAAA,CACA,qCAAA,CACA,mCAAA,CACA,SAAA,CAEA,6BACE,SAAA,CAGF,8CACE,yBAAA,CACA,4BAAA,CAEA,kDACE,wBAAA,CAGJ,6CACE,wBAAA,CACA,2BAAA,CAIJ,qBACE,qBAAA,CACA,oCAAA,CAGF,sDACE,SAAA,CCpCF,yBACE,iBAAA,CACA,yBAAA,CACA,8BAAA,CACA,iBAAA,CACA,mBAAA,CACA,iCAAA,CACA,QAAA,CACA,0BAAA,CACA,kBAAA,CACA,SAAA,CACA,YAAA,CACA,SAAA,CACA,kBAAA,CACA,8BACE,oBAAA,CACA,WAAA,CACA,YAAA,CACA,uBAAA,CACA,6BAAA,CACA,WAAA,CACA,6BAAA,CAEA,8CACE,UAAA,CCxBN,gBACE,iBAAA,CACA,iBAAA,CACA,YAAA,CACA,iEAAA,CACA,6BAAA,CACA,2BAAA,CACA,UAAA,CACA,WAAA,CACA,cAAA,CACA,eAAA,CACA,0BAAA,CAEA,yCACE,yCAAA,CCdJ,iBACE,YAAA,CACA,6BAAA,CACA,sBAAA,CACA,UAAA,CACA,eAAA,CACA,0BAAA,CAGF,wBACE,iBAAA,CAEA,sCACE,iBAAA,CACA,QAAA,CACA,UAAA,CACA,SAAA,CACA,WAAA,CAIJ,yBACE,iBACE,yBAAA,CACA,0BAAA,CAEF,aACE,OAAA,CAAA,CC3BJ,aACE,eAAA,CACA,YAAA,CACA,sBAAA,CAEA,eAEE,aAAA,CACA,kBAAA,CACA,uBAAA,CAGF,2CACE,qBAAA,CACA,4BAAA,CACA,eAAA,CACA,iBAAA,CAEA,6CACE,qGAAA,CACA,oEAAA,CAIJ,6CACE,4BAAA,CACA,iBAAA,CACA,eAAA,CAEA,+CACE,0FAAA,CACA,kEAAA,CC/BN,uBACE,YAAA,CACA,aAAA,CCFF,sEAEE,iBAAA,CACA,oCAAA,CACA,qCAAA,CACA,wBAAA,CAGA,mCAAA,CAMA,qDACE,UAAA,CAIJ,qCACE,2BAAA,CAEA,yDACE,UAAA,CCvBJ,qBACE,iBAAA,CCDF,4BACE,UAAA,CACA,WAAA,CACA,gBAAA,CACA,sBAAA,CAEA,qBAAA,CAEA,2DACE,gBAAA,CAIF,mHAEE,kBAAA,CACA,8BAAA,CAIJ,4BACE,UAAA,CAIF,8GAEE,yBAAA,CCxBF,qBACE,UAAA,CACA,WAAA,CACA,gBAAA,CACA,+BAAA,CACA,iBAAA,CAEA,mBAAA,CACA,YAAA,CACA,4BAAA,CACA,kBAAA,CAGA,0BACE,wBAAA,CAGA,4DAAA,CAGF,0BACE,0BAAA,CAGA,kDAAA,CAIJ,yBACE,YAAA,CACA,kBAAA,CACA,sBAAA,CACA,UAAA,CACA,WAAA,CACA,uBAAA,CACA,8BAAA,CAEA,uCACE,yBAAA,CACA,WAAA,CACA,kCAAA,CACA,kDAAA,CACA,8DAAA,CACA,yCAAA,CAIF,4KAGE,iDAAA,CACA,eAAA,CASF,oDACE,yCAAA,CChEJ,qBACE,oCAAA,CACA,iBAAA,CACA,YAAA,CACA,qBAAA,CACA,WAAA,CACA,eAAA,CACA,qCAAA,CACA,4BACE,UAAA,CACA,iBAAA,CACA,KAAA,CACA,QAAA,CACA,MAAA,CACA,OAAA,CACA,qCAAA,CACA,oCAAA,CAEA,kCAAA,CACA,oBAAA,CACA,uBAAA,CACA,mBAAA,CAEF,sFAEE,mBAAA,CACA,uBAAA,CACA,+CAAA,CAGF,6CACE,iBAAA,CACA,UAAA,CACA,YAAA,CACA,cAAA,CACA,+BAAA,CACA,6CAAA,CACA,SAAA,CACA,kCAAA,CACA,oBAAA,CAGF,sGAEE,SAAA,CACA,kBAAA,CAGF,4CACE,SAAA,CACA,kCAAA,CACA,oBAAA,CAEA,kEACE,SAAA,CACA,kBAAA,CAIJ,oGAEE,SAAA,CACA,kBAAA,CAGF,iDACE,iBAAA,CACA,OAAA,CACA,YAAA,CACA,kBAAA,CACA,sBAAA,CACA,8BAAA,CACA,SAAA,CACA,kCAAA,CACA,mBAAA,CACA,qCAAA,CAEA,qDACE,WAAA,CACA,UAAA,CACA,WAAA,CAIJ,kGACE,SAAA,CAIJ,yBACE,iBAAA,CACA,YAAA,CACA,aAAA,CACA,WAAA,CACA,YAAA,CACA,kBAAA,CACA,kBAAA,CACA,6BAAA,CACA,SAAA,CACA,aAAA,CAGF,8BACE,YAAA,CACA,kBAAA,CACA,cAAA,CACA,+BAAA,CACA,6CAAA,CC3GF,UACE,cAAA,CACA,UAAA,CACA,QAAA,CACA,0BAAA,CACA,YAAA,CACA,kBAAA,CACA,SAAA,CACA,sBAAA,CACA,6BAAA,CACA,uCAAA,CACA,qCAAA,CACA,+BAAA,CAGF,YACE,sCAAA,CAGF,qBACE,KACE,sBAAA,CAEF,GACE,wBAAA,CAAA,ClBxBJ,mBACE,6BAAA,CACA,iCAAA,CAGF,gBACE,6BAAA,CACA,iCAAA,CACA,kBAAA,CAEA,yBACE,eAAA,CACA,QAAA,CACA,SAAA,CAGF,iCACE,iCAAA,CACA,oBAAA,CACA,qCAAA,CACA,uCAAA,CACA,qCAAA,CmBnBJ,mBAEE,iBAAA,CACA,UAAA,CACA,WAAA,CACA,gCAAA,CAEA,qEAEE,qBAAA,CCXJ,qBACE,iBAAA,CACA,UAAA,CACA,WAAA,CAGF,2BACE,UAAA,CACA,WAAA,CACA,YAAA,CACA,oCAAA,CACA,QAAA,CCXF,SACE,YAAA,CACA,gFAAA,CACA,4BAAA,CACA,8BAAA,CACA,4CAAA,CACA,eAAA,CAGF,gBACE,mCAAA,CACA,cAAA,CACA,iBAAA,CACA,YAAA,CACA,kBAAA,CACA,sBAAA,CACA,iCACE,iBAAA,CACA,OAAA,CACA,0BAAA,CACA,8BAAA,CACA,uCACE,kDAAA,CAKN,kBACE,YAAA,CACA,UAAA,CACA,eAAA,CACA,qBAAA,CACA,UAAA,CACA,aAAA,CAGF,eACE,YAAA,CACA,qBAAA,CACA,UAAA,CACA,eAAA,CAEA,6BACE,gBAAA,CACA,mBAAA,CACA,kBAAA,CACA,eAAA,CACA,YAAA,CAEA,kDACE,eAAA,CAGF,2CACE,gBAAA,CACA,mBAAA,CAIJ,+BACE,eAAA,CACA,WAAA,CACA,QAAA,CACA,gBAAA,CACA,eAAA,CACA,cAAA,CAGF,gCACE,oBAAA,CACA,kBAAA,CACA,qBAAA,CACA,qBAAA,CACA,iBAAA,CACA,2BAAA,CAIA,8DACE,8BAAA,CAKF,+DACE,kCAAA,CAIJ,iBACE,yBAAA,CACA,aAAA,CAGF,iBACE,wBAAA,CACA,sBAAA,CAGF,0BACE,oBAAA,CAIJ,cACE,YAAA,CACA,UAAA,CACA,cAAA,CACA,2CAAA,CACA,uCAAA,CAGF,oBACE,iBAAA,CACA,mBAAA,CACA,UAAA,CAGF,yBACE,SACE,cAAA,CACA,KAAA,CACA,OAAA,CACA,cAAA,CACA,mCAAA,CAAA,CC5HJ,sCACE,YAAA,CACA,SAAA,CACA,kBAAA,CACA,sBAAA,CACA,cAAA,CACA,2CAAA,CACA,uCAAA,CAGF,sBAGE,mCAAA,CACA,oBAAA,CACA,sBAAA,CACA,oBAAA,CACA,8BAAA,CAEE,4EACE,WAAA,CtBpBN,YACE,6BAAA,CACA,iCAAA,CAGF,gBACE,6BAAA,CACA,iCAAA,CACA,kBAAA,CAEA,yBACE,eAAA,CACA,QAAA,CACA,SAAA,CAGF,iCACE,iCAAA,CACA,oBAAA,CACA,qCAAA,CACA,uCAAA,CACA,qCAAA,CuBnBJ,YAEE,qBAAA,CACA,YAAA,CACA,qBAAA,CACA,kBAAA,CACA,YAAA,CACA,QAAA,CACA,kBAAA,CACA,6BAAA,CACA,qBAAA,CACA,mBAAA,CAEA,gCACE,iBAAA,CACA,UAAA,CACA,WAAA,CACA,kBAAA,CACA,qBAAA,CACA,qCAAA,CACA,eAAA,CACA,0FAEE,aAAA,CACA,UAAA,CACA,WAAA,CACA,gBAAA,CAGF,gEACE,yBAAA,CAGF,oDACE,iBAAA,CACA,OAAA,CACA,QAAA,CACA,UAAA,CACA,kBAAA,CACA,qBAAA,CAEA,YAAA,CACA,kBAAA,CACA,sDACE,UAAA,CACA,cAAA,CAKN,gCACE,YAAA,CACA,sCACE,UAAA,CACA,WAAA,CAIJ,uCACE,YAAA,CACA,gBAAA,CACA,QAAA,CAEA,wDACE,SAAA,CAEA,mEACE,oBAAA,CAGF,+EACE,UAAA,CAIN,yBACE,uCACE,cAAA,CACA,wDACE,UAAA,CAAA,CAKN,mCACE,YAAA,CACA,qBAAA,CACA,QAAA,CACA,UAAA,CACA,cAAA,CAGF,4BACE,oCAAA,CACA,oCAAA,CACA,wCAAA,CACA,yCAAA,CACA,+CAAA,CACA,qCAAA,CACA,kCACE,2CAAA,CCtGN,iDAEE,iBAAA,CACA,YAAA,CACA,sBAAA,CACA,UAAA,CACA,gDAAA,CAGF,wBACE,qBAAA,CACA,kBAAA,CAGF,yBACE,mBAAA,CAGF,qBACE,iBAAA,CACA,YAAA,CACA,mBAAA,CACA,WAAA,CAGF,2BACE,YAAA,CACA,qBAAA,CACA,mBAAA,CACA,UAAA,CAGF,wBACE,cAAA,CACA,OAAA,CACA,QAAA,CACA,+BAAA,CACA,uBAAA,CACA,YAAA,CACA,qCAAA,CAEA,YAAA,CACA,qBAAA,CACA,kBAAA,CACA,SAAA,CACA,sBAAA,CACA,6BAAA,CACA,uCAAA,CACA,qCAAA,CACA,+BAAA,CACA,cAAA,CACA,eAAA,CACA,cAAA,CACA,eAAA,CACA,eAAA","file":"index.css"}
No newline at end of file
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_button.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_button.scss.d.ts +12 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_button.scss.d.ts
@@ -0,0 +1,12 @@
1export type Styles = {
2 button: string;
3 'button-group': string;
4 'button-group-menu': string;
5 'button-menu': string;
6};
7
8export type UnprefixedClassNames = keyof Styles;
9
10declare const styles: Styles;
11
12export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_chat-toggle.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_chat-toggle.scss.d.ts +13 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_chat-toggle.scss.d.ts
@@ -0,0 +1,13 @@
1export type Styles = {
2 button: string;
3 'button-group': string;
4 'button-group-menu': string;
5 'button-menu': string;
6 'chat-toggle': string;
7};
8
9export type UnprefixedClassNames = keyof Styles;
10
11declare const styles: Styles;
12
13export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_disconnect-button.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_disconnect-button.scss.d.ts +13 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_disconnect-button.scss.d.ts
@@ -0,0 +1,13 @@
1export type Styles = {
2 button: string;
3 'button-group': string;
4 'button-group-menu': string;
5 'button-menu': string;
6 'disconnect-button': string;
7};
8
9export type UnprefixedClassNames = keyof Styles;
10
11declare const styles: Styles;
12
13export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_media-device-select.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_media-device-select.scss.d.ts +12 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_media-device-select.scss.d.ts
@@ -0,0 +1,12 @@
1export type Styles = {
2 button: string;
3 'device-menu': string;
4 'device-menu-heading': string;
5 'media-device-select': string;
6};
7
8export type UnprefixedClassNames = keyof Styles;
9
10declare const styles: Styles;
11
12export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_pagination-control.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_pagination-control.scss.d.ts +11 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_pagination-control.scss.d.ts
@@ -0,0 +1,11 @@
1export type Styles = {
2 button: string;
3 'pagination-control': string;
4 'pagination-count': string;
5};
6
7export type UnprefixedClassNames = keyof Styles;
8
9declare const styles: Styles;
10
11export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_pagination-indicator.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_pagination-indicator.scss.d.ts +9 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_pagination-indicator.scss.d.ts
@@ -0,0 +1,9 @@
1export type Styles = {
2 'pagination-indicator': string;
3};
4
5export type UnprefixedClassNames = keyof Styles;
6
7declare const styles: Styles;
8
9export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_start-audio.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_start-audio.scss.d.ts +13 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/_start-audio.scss.d.ts
@@ -0,0 +1,13 @@
1export type Styles = {
2 button: string;
3 'button-group': string;
4 'button-group-menu': string;
5 'button-menu': string;
6 'start-audio-button': string;
7};
8
9export type UnprefixedClassNames = keyof Styles;
10
11declare const styles: Styles;
12
13export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/index.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/index.scss.d.ts +21 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/controls/index.scss.d.ts
@@ -0,0 +1,21 @@
1export type Styles = {
2 button: string;
3 'button-group': string;
4 'button-group-menu': string;
5 'button-menu': string;
6 'chat-toggle': string;
7 'device-menu': string;
8 'device-menu-heading': string;
9 'disconnect-button': string;
10 'media-device-select': string;
11 'pagination-control': string;
12 'pagination-count': string;
13 'pagination-indicator': string;
14 'start-audio-button': string;
15};
16
17export type UnprefixedClassNames = keyof Styles;
18
19declare const styles: Styles;
20
21export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/layout/_carousel-view.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/layout/_carousel-view.scss.d.ts +9 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/layout/_carousel-view.scss.d.ts
@@ -0,0 +1,9 @@
1export type Styles = {
2 carousel: string;
3};
4
5export type UnprefixedClassNames = keyof Styles;
6
7declare const styles: Styles;
8
9export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/layout/_focus-layout.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/layout/_focus-layout.scss.d.ts +12 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/layout/_focus-layout.scss.d.ts
@@ -0,0 +1,12 @@
1export type Styles = {
2 carousel: string;
3 'focus-layout': string;
4 'focused-participant': string;
5 'pip-track': string;
6};
7
8export type UnprefixedClassNames = keyof Styles;
9
10declare const styles: Styles;
11
12export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/layout/_grid-layout.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/layout/_grid-layout.scss.d.ts +9 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/layout/_grid-layout.scss.d.ts
@@ -0,0 +1,9 @@
1export type Styles = {
2 'grid-layout': string;
3};
4
5export type UnprefixedClassNames = keyof Styles;
6
7declare const styles: Styles;
8
9export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/layout/index.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/layout/index.scss.d.ts +13 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/layout/index.scss.d.ts
@@ -0,0 +1,13 @@
1export type Styles = {
2 carousel: string;
3 'focus-layout': string;
4 'focused-participant': string;
5 'grid-layout': string;
6 'pip-track': string;
7};
8
9export type UnprefixedClassNames = keyof Styles;
10
11declare const styles: Styles;
12
13export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_audio-visualizer.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_audio-visualizer.scss.d.ts +12 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_audio-visualizer.scss.d.ts
@@ -0,0 +1,12 @@
1export type Styles = {
2 'audio-bar': string;
3 'audio-bar-visualizer': string;
4 'audio-visualizer': string;
5 highlighted: string;
6};
7
8export type UnprefixedClassNames = keyof Styles;
9
10declare const styles: Styles;
11
12export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_connection-quality.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_connection-quality.scss.d.ts +9 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_connection-quality.scss.d.ts
@@ -0,0 +1,9 @@
1export type Styles = {
2 'connection-quality': string;
3};
4
5export type UnprefixedClassNames = keyof Styles;
6
7declare const styles: Styles;
8
9export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_participant-media.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_participant-media.scss.d.ts +10 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_participant-media.scss.d.ts
@@ -0,0 +1,10 @@
1export type Styles = {
2 'participant-media-audio': string;
3 'participant-media-video': string;
4};
5
6export type UnprefixedClassNames = keyof Styles;
7
8declare const styles: Styles;
9
10export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_participant-name.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_participant-name.scss.d.ts +9 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_participant-name.scss.d.ts
@@ -0,0 +1,9 @@
1export type Styles = {
2 'participant-name': string;
3};
4
5export type UnprefixedClassNames = keyof Styles;
6
7declare const styles: Styles;
8
9export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_participant-tile.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_participant-tile.scss.d.ts +14 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_participant-tile.scss.d.ts
@@ -0,0 +1,14 @@
1export type Styles = {
2 'connection-quality': string;
3 'focus-toggle-button': string;
4 'participant-metadata': string;
5 'participant-metadata-item': string;
6 'participant-placeholder': string;
7 'participant-tile': string;
8};
9
10export type UnprefixedClassNames = keyof Styles;
11
12declare const styles: Styles;
13
14export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_track-muted-indicator.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_track-muted-indicator.scss.d.ts +10 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/_track-muted-indicator.scss.d.ts
@@ -0,0 +1,10 @@
1export type Styles = {
2 'track-muted-indicator-camera': string;
3 'track-muted-indicator-microphone': string;
4};
5
6export type UnprefixedClassNames = keyof Styles;
7
8declare const styles: Styles;
9
10export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/index.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/index.scss.d.ts +23 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/participant/index.scss.d.ts
@@ -0,0 +1,23 @@
1export type Styles = {
2 'audio-bar': string;
3 'audio-bar-visualizer': string;
4 'audio-visualizer': string;
5 'connection-quality': string;
6 'focus-toggle-button': string;
7 highlighted: string;
8 'participant-media-audio': string;
9 'participant-media-video': string;
10 'participant-metadata': string;
11 'participant-metadata-item': string;
12 'participant-name': string;
13 'participant-placeholder': string;
14 'participant-tile': string;
15 'track-muted-indicator-camera': string;
16 'track-muted-indicator-microphone': string;
17};
18
19export type UnprefixedClassNames = keyof Styles;
20
21declare const styles: Styles;
22
23export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/_room.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/_room.scss.d.ts +11 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/_room.scss.d.ts
@@ -0,0 +1,11 @@
1export type Styles = {
2 'form-control': string;
3 list: string;
4 'room-container': string;
5};
6
7export type UnprefixedClassNames = keyof Styles;
8
9declare const styles: Styles;
10
11export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/_toast.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/_toast.scss.d.ts +11 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/_toast.scss.d.ts
@@ -0,0 +1,11 @@
1export type Styles = {
2 'lk-rotate': string;
3 spinner: string;
4 toast: string;
5};
6
7export type UnprefixedClassNames = keyof Styles;
8
9declare const styles: Styles;
10
11export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/index.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/index.scss.d.ts +47 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/components/index.scss.d.ts
@@ -0,0 +1,47 @@
1export type Styles = {
2 'audio-bar': string;
3 'audio-bar-visualizer': string;
4 'audio-visualizer': string;
5 button: string;
6 'button-group': string;
7 'button-group-menu': string;
8 'button-menu': string;
9 carousel: string;
10 'chat-toggle': string;
11 'connection-quality': string;
12 'device-menu': string;
13 'device-menu-heading': string;
14 'disconnect-button': string;
15 'focus-layout': string;
16 'focus-toggle-button': string;
17 'focused-participant': string;
18 'form-control': string;
19 'grid-layout': string;
20 highlighted: string;
21 list: string;
22 'lk-rotate': string;
23 'media-device-select': string;
24 'pagination-control': string;
25 'pagination-count': string;
26 'pagination-indicator': string;
27 'participant-media-audio': string;
28 'participant-media-video': string;
29 'participant-metadata': string;
30 'participant-metadata-item': string;
31 'participant-name': string;
32 'participant-placeholder': string;
33 'participant-tile': string;
34 'pip-track': string;
35 'room-container': string;
36 spinner: string;
37 'start-audio-button': string;
38 toast: string;
39 'track-muted-indicator-camera': string;
40 'track-muted-indicator-microphone': string;
41};
42
43export type UnprefixedClassNames = keyof Styles;
44
45declare const styles: Styles;
46
47export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/audio-conference.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/audio-conference.scss.d.ts +10 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/audio-conference.scss.d.ts
@@ -0,0 +1,10 @@
1export type Styles = {
2 'audio-conference': string;
3 'audio-conference-stage': string;
4};
5
6export type UnprefixedClassNames = keyof Styles;
7
8declare const styles: Styles;
9
10export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/chat.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/chat.scss.d.ts +20 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/chat.scss.d.ts
@@ -0,0 +1,20 @@
1export type Styles = {
2 chat: string;
3 'chat-entry': string;
4 'chat-form': string;
5 'chat-form-input': string;
6 'chat-header': string;
7 'chat-messages': string;
8 'close-button': string;
9 'edit-button': string;
10 'message-body': string;
11 'meta-data': string;
12 'participant-name': string;
13 timestamp: string;
14};
15
16export type UnprefixedClassNames = keyof Styles;
17
18declare const styles: Styles;
19
20export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/control-bar.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/control-bar.scss.d.ts +13 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/control-bar.scss.d.ts
@@ -0,0 +1,13 @@
1export type Styles = {
2 'agent-control-bar': string;
3 'audio-bar': string;
4 'audio-bar-visualizer': string;
5 'control-bar': string;
6 highlighted: string;
7};
8
9export type UnprefixedClassNames = keyof Styles;
10
11declare const styles: Styles;
12
13export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/index.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/index.scss.d.ts +43 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/index.scss.d.ts
@@ -0,0 +1,43 @@
1export type Styles = {
2 'agent-control-bar': string;
3 'audio-bar': string;
4 'audio-bar-visualizer': string;
5 'audio-conference': string;
6 'audio-conference-stage': string;
7 'audio-container': string;
8 button: string;
9 'button-group': string;
10 'button-group-container': string;
11 'camera-off-note': string;
12 chat: string;
13 'chat-entry': string;
14 'chat-form': string;
15 'chat-form-input': string;
16 'chat-header': string;
17 'chat-messages': string;
18 'close-button': string;
19 'control-bar': string;
20 'edit-button': string;
21 'focus-layout-wrapper': string;
22 'form-control': string;
23 'grid-layout-wrapper': string;
24 highlighted: string;
25 'join-button': string;
26 list: string;
27 'message-body': string;
28 'meta-data': string;
29 'participant-name': string;
30 prejoin: string;
31 'settings-menu-modal': string;
32 timestamp: string;
33 'username-container': string;
34 'video-conference': string;
35 'video-conference-inner': string;
36 'video-container': string;
37};
38
39export type UnprefixedClassNames = keyof Styles;
40
41declare const styles: Styles;
42
43export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/prejoin.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/prejoin.scss.d.ts +19 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/prejoin.scss.d.ts
@@ -0,0 +1,19 @@
1export type Styles = {
2 'audio-container': string;
3 button: string;
4 'button-group': string;
5 'button-group-container': string;
6 'camera-off-note': string;
7 'form-control': string;
8 'join-button': string;
9 list: string;
10 prejoin: string;
11 'username-container': string;
12 'video-container': string;
13};
14
15export type UnprefixedClassNames = keyof Styles;
16
17declare const styles: Styles;
18
19export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/video-conference.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/video-conference.scss.d.ts +13 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/prefabs/video-conference.scss.d.ts
@@ -0,0 +1,13 @@
1export type Styles = {
2 'focus-layout-wrapper': string;
3 'grid-layout-wrapper': string;
4 'settings-menu-modal': string;
5 'video-conference': string;
6 'video-conference-inner': string;
7};
8
9export type UnprefixedClassNames = keyof Styles;
10
11declare const styles: Styles;
12
13export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/themes/huddle.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/themes/huddle.scss.d.ts +11 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/themes/huddle.scss.d.ts
@@ -0,0 +1,11 @@
1export type Styles = {
2 button: string;
3 'disconnect-button': string;
4 'join-button': string;
5};
6
7export type UnprefixedClassNames = keyof Styles;
8
9declare const styles: Styles;
10
11export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/_preflight.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/_preflight.scss.d.ts +10 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/_preflight.scss.d.ts
@@ -0,0 +1,10 @@
1export type Styles = {
2 'form-control': string;
3 list: string;
4};
5
6export type UnprefixedClassNames = keyof Styles;
7
8declare const styles: Styles;
9
10export default styles;
Added view/app/src/vendor/livekit/styles/dist/types_unprefixed/index.scss.d.ts view/app/src/vendor/livekit/styles/dist/types_unprefixed/index.scss.d.ts +74 −0 View view/app/src/vendor/livekit/styles/dist/types_unprefixed/index.scss.d.ts
@@ -0,0 +1,74 @@
1export type Styles = {
2 'agent-control-bar': string;
3 'audio-bar': string;
4 'audio-bar-visualizer': string;
5 'audio-conference': string;
6 'audio-conference-stage': string;
7 'audio-container': string;
8 'audio-visualizer': string;
9 button: string;
10 'button-group': string;
11 'button-group-container': string;
12 'button-group-menu': string;
13 'button-menu': string;
14 'camera-off-note': string;
15 carousel: string;
16 chat: string;
17 'chat-entry': string;
18 'chat-form': string;
19 'chat-form-input': string;
20 'chat-header': string;
21 'chat-messages': string;
22 'chat-toggle': string;
23 'close-button': string;
24 'connection-quality': string;
25 'control-bar': string;
26 'device-menu': string;
27 'device-menu-heading': string;
28 'disconnect-button': string;
29 'edit-button': string;
30 'focus-layout': string;
31 'focus-layout-wrapper': string;
32 'focus-toggle-button': string;
33 'focused-participant': string;
34 'form-control': string;
35 'grid-layout': string;
36 'grid-layout-wrapper': string;
37 highlighted: string;
38 'join-button': string;
39 list: string;
40 'lk-rotate': string;
41 'media-device-select': string;
42 'message-body': string;
43 'meta-data': string;
44 'pagination-control': string;
45 'pagination-count': string;
46 'pagination-indicator': string;
47 'participant-media-audio': string;
48 'participant-media-video': string;
49 'participant-metadata': string;
50 'participant-metadata-item': string;
51 'participant-name': string;
52 'participant-placeholder': string;
53 'participant-tile': string;
54 'pip-track': string;
55 prejoin: string;
56 'room-container': string;
57 'settings-menu-modal': string;
58 spinner: string;
59 'start-audio-button': string;
60 timestamp: string;
61 toast: string;
62 'track-muted-indicator-camera': string;
63 'track-muted-indicator-microphone': string;
64 'username-container': string;
65 'video-conference': string;
66 'video-conference-inner': string;
67 'video-container': string;
68};
69
70export type UnprefixedClassNames = keyof Styles;
71
72declare const styles: Styles;
73
74export default styles;
Added view/app/src/vendor/livekit/index.ts view/app/src/vendor/livekit/index.ts +14 −0 View view/app/src/vendor/livekit/index.ts
@@ -0,0 +1,14 @@
1/**
2 * Public API of the vendored LiveKit component tree (`react/`, `core/`,
3 * `styles/` are byte-copied from the published npm packages).
4 *
5 * Consumers import from `@/shared/livekit` and never from deeper files, so
6 * replaying a newer official tarball only has to keep this surface intact.
7 */
8export * from './react'
9
10// A few helpers the official barrel keeps internal, but a hand-built room view
11// needs in order to key and compare track references.
12export { getTrackReferenceId, isEqualTrackRef } from '@livekit/components-core'
13
14export { lkT } from './lk-i18n'
Added view/app/src/vendor/livekit/LICENSE-components-core view/app/src/vendor/livekit/LICENSE-components-core +201 −0 View view/app/src/vendor/livekit/LICENSE-components-core
@@ -0,0 +1,201 @@
1 Apache License
2 Version 2.0, January 2004
3 http://www.apache.org/licenses/
4
5 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
7 1. Definitions.
8
9 "License" shall mean the terms and conditions for use, reproduction,
10 and distribution as defined by Sections 1 through 9 of this document.
11
12 "Licensor" shall mean the copyright owner or entity authorized by
13 the copyright owner that is granting the License.
14
15 "Legal Entity" shall mean the union of the acting entity and all
16 other entities that control, are controlled by, or are under common
17 control with that entity. For the purposes of this definition,
18 "control" means (i) the power, direct or indirect, to cause the
19 direction or management of such entity, whether by contract or
20 otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 outstanding shares, or (iii) beneficial ownership of such entity.
22
23 "You" (or "Your") shall mean an individual or Legal Entity
24 exercising permissions granted by this License.
25
26 "Source" form shall mean the preferred form for making modifications,
27 including but not limited to software source code, documentation
28 source, and configuration files.
29
30 "Object" form shall mean any form resulting from mechanical
31 transformation or translation of a Source form, including but
32 not limited to compiled object code, generated documentation,
33 and conversions to other media types.
34
35 "Work" shall mean the work of authorship, whether in Source or
36 Object form, made available under the License, as indicated by a
37 copyright notice that is included in or attached to the work
38 (an example is provided in the Appendix below).
39
40 "Derivative Works" shall mean any work, whether in Source or Object
41 form, that is based on (or derived from) the Work and for which the
42 editorial revisions, annotations, elaborations, or other modifications
43 represent, as a whole, an original work of authorship. For the purposes
44 of this License, Derivative Works shall not include works that remain
45 separable from, or merely link (or bind by name) to the interfaces of,
46 the Work and Derivative Works thereof.
47
48 "Contribution" shall mean any work of authorship, including
49 the original version of the Work and any modifications or additions
50 to that Work or Derivative Works thereof, that is intentionally
51 submitted to Licensor for inclusion in the Work by the copyright owner
52 or by an individual or Legal Entity authorized to submit on behalf of
53 the copyright owner. For the purposes of this definition, "submitted"
54 means any form of electronic, verbal, or written communication sent
55 to the Licensor or its representatives, including but not limited to
56 communication on electronic mailing lists, source code control systems,
57 and issue tracking systems that are managed by, or on behalf of, the
58 Licensor for the purpose of discussing and improving the Work, but
59 excluding communication that is conspicuously marked or otherwise
60 designated in writing by the copyright owner as "Not a Contribution."
61
62 "Contributor" shall mean Licensor and any individual or Legal Entity
63 on behalf of whom a Contribution has been received by Licensor and
64 subsequently incorporated within the Work.
65
66 2. Grant of Copyright License. Subject to the terms and conditions of
67 this License, each Contributor hereby grants to You a perpetual,
68 worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 copyright license to reproduce, prepare Derivative Works of,
70 publicly display, publicly perform, sublicense, and distribute the
71 Work and such Derivative Works in Source or Object form.
72
73 3. Grant of Patent License. Subject to the terms and conditions of
74 this License, each Contributor hereby grants to You a perpetual,
75 worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 (except as stated in this section) patent license to make, have made,
77 use, offer to sell, sell, import, and otherwise transfer the Work,
78 where such license applies only to those patent claims licensable
79 by such Contributor that are necessarily infringed by their
80 Contribution(s) alone or by combination of their Contribution(s)
81 with the Work to which such Contribution(s) was submitted. If You
82 institute patent litigation against any entity (including a
83 cross-claim or counterclaim in a lawsuit) alleging that the Work
84 or a Contribution incorporated within the Work constitutes direct
85 or contributory patent infringement, then any patent licenses
86 granted to You under this License for that Work shall terminate
87 as of the date such litigation is filed.
88
89 4. Redistribution. You may reproduce and distribute copies of the
90 Work or Derivative Works thereof in any medium, with or without
91 modifications, and in Source or Object form, provided that You
92 meet the following conditions:
93
94 (a) You must give any other recipients of the Work or
95 Derivative Works a copy of this License; and
96
97 (b) You must cause any modified files to carry prominent notices
98 stating that You changed the files; and
99
100 (c) You must retain, in the Source form of any Derivative Works
101 that You distribute, all copyright, patent, trademark, and
102 attribution notices from the Source form of the Work,
103 excluding those notices that do not pertain to any part of
104 the Derivative Works; and
105
106 (d) If the Work includes a "NOTICE" text file as part of its
107 distribution, then any Derivative Works that You distribute must
108 include a readable copy of the attribution notices contained
109 within such NOTICE file, excluding those notices that do not
110 pertain to any part of the Derivative Works, in at least one
111 of the following places: within a NOTICE text file distributed
112 as part of the Derivative Works; within the Source form or
113 documentation, if provided along with the Derivative Works; or,
114 within a display generated by the Derivative Works, if and
115 wherever such third-party notices normally appear. The contents
116 of the NOTICE file are for informational purposes only and
117 do not modify the License. You may add Your own attribution
118 notices within Derivative Works that You distribute, alongside
119 or as an addendum to the NOTICE text from the Work, provided
120 that such additional attribution notices cannot be construed
121 as modifying the License.
122
123 You may add Your own copyright statement to Your modifications and
124 may provide additional or different license terms and conditions
125 for use, reproduction, or distribution of Your modifications, or
126 for any such Derivative Works as a whole, provided Your use,
127 reproduction, and distribution of the Work otherwise complies with
128 the conditions stated in this License.
129
130 5. Submission of Contributions. Unless You explicitly state otherwise,
131 any Contribution intentionally submitted for inclusion in the Work
132 by You to the Licensor shall be under the terms and conditions of
133 this License, without any additional terms or conditions.
134 Notwithstanding the above, nothing herein shall supersede or modify
135 the terms of any separate license agreement you may have executed
136 with Licensor regarding such Contributions.
137
138 6. Trademarks. This License does not grant permission to use the trade
139 names, trademarks, service marks, or product names of the Licensor,
140 except as required for reasonable and customary use in describing the
141 origin of the Work and reproducing the content of the NOTICE file.
142
143 7. Disclaimer of Warranty. Unless required by applicable law or
144 agreed to in writing, Licensor provides the Work (and each
145 Contributor provides its Contributions) on an "AS IS" BASIS,
146 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 implied, including, without limitation, any warranties or conditions
148 of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 PARTICULAR PURPOSE. You are solely responsible for determining the
150 appropriateness of using or redistributing the Work and assume any
151 risks associated with Your exercise of permissions under this License.
152
153 8. Limitation of Liability. In no event and under no legal theory,
154 whether in tort (including negligence), contract, or otherwise,
155 unless required by applicable law (such as deliberate and grossly
156 negligent acts) or agreed to in writing, shall any Contributor be
157 liable to You for damages, including any direct, indirect, special,
158 incidental, or consequential damages of any character arising as a
159 result of this License or out of the use or inability to use the
160 Work (including but not limited to damages for loss of goodwill,
161 work stoppage, computer failure or malfunction, or any and all
162 other commercial damages or losses), even if such Contributor
163 has been advised of the possibility of such damages.
164
165 9. Accepting Warranty or Additional Liability. While redistributing
166 the Work or Derivative Works thereof, You may choose to offer,
167 and charge a fee for, acceptance of support, warranty, indemnity,
168 or other liability obligations and/or rights consistent with this
169 License. However, in accepting such obligations, You may act only
170 on Your own behalf and on Your sole responsibility, not on behalf
171 of any other Contributor, and only if You agree to indemnify,
172 defend, and hold each Contributor harmless for any liability
173 incurred by, or claims asserted against, such Contributor by reason
174 of your accepting any such warranty or additional liability.
175
176 END OF TERMS AND CONDITIONS
177
178 APPENDIX: How to apply the Apache License to your work.
179
180 To apply the Apache License to your work, attach the following
181 boilerplate notice, with the fields enclosed by brackets "[]"
182 replaced with your own identifying information. (Don't include
183 the brackets!) The text should be enclosed in the appropriate
184 comment syntax for the file format. We also recommend that a
185 file or class name and description of purpose be included on the
186 same "printed page" as the copyright notice for easier
187 identification within third-party archives.
188
189 Copyright [yyyy] [name of copyright owner]
190
191 Licensed under the Apache License, Version 2.0 (the "License");
192 you may not use this file except in compliance with the License.
193 You may obtain a copy of the License at
194
195 http://www.apache.org/licenses/LICENSE-2.0
196
197 Unless required by applicable law or agreed to in writing, software
198 distributed under the License is distributed on an "AS IS" BASIS,
199 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 See the License for the specific language governing permissions and
201 limitations under the License.
Added view/app/src/vendor/livekit/LICENSE-components-react view/app/src/vendor/livekit/LICENSE-components-react +201 −0 View view/app/src/vendor/livekit/LICENSE-components-react
@@ -0,0 +1,201 @@
1 Apache License
2 Version 2.0, January 2004
3 http://www.apache.org/licenses/
4
5 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
7 1. Definitions.
8
9 "License" shall mean the terms and conditions for use, reproduction,
10 and distribution as defined by Sections 1 through 9 of this document.
11
12 "Licensor" shall mean the copyright owner or entity authorized by
13 the copyright owner that is granting the License.
14
15 "Legal Entity" shall mean the union of the acting entity and all
16 other entities that control, are controlled by, or are under common
17 control with that entity. For the purposes of this definition,
18 "control" means (i) the power, direct or indirect, to cause the
19 direction or management of such entity, whether by contract or
20 otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 outstanding shares, or (iii) beneficial ownership of such entity.
22
23 "You" (or "Your") shall mean an individual or Legal Entity
24 exercising permissions granted by this License.
25
26 "Source" form shall mean the preferred form for making modifications,
27 including but not limited to software source code, documentation
28 source, and configuration files.
29
30 "Object" form shall mean any form resulting from mechanical
31 transformation or translation of a Source form, including but
32 not limited to compiled object code, generated documentation,
33 and conversions to other media types.
34
35 "Work" shall mean the work of authorship, whether in Source or
36 Object form, made available under the License, as indicated by a
37 copyright notice that is included in or attached to the work
38 (an example is provided in the Appendix below).
39
40 "Derivative Works" shall mean any work, whether in Source or Object
41 form, that is based on (or derived from) the Work and for which the
42 editorial revisions, annotations, elaborations, or other modifications
43 represent, as a whole, an original work of authorship. For the purposes
44 of this License, Derivative Works shall not include works that remain
45 separable from, or merely link (or bind by name) to the interfaces of,
46 the Work and Derivative Works thereof.
47
48 "Contribution" shall mean any work of authorship, including
49 the original version of the Work and any modifications or additions
50 to that Work or Derivative Works thereof, that is intentionally
51 submitted to Licensor for inclusion in the Work by the copyright owner
52 or by an individual or Legal Entity authorized to submit on behalf of
53 the copyright owner. For the purposes of this definition, "submitted"
54 means any form of electronic, verbal, or written communication sent
55 to the Licensor or its representatives, including but not limited to
56 communication on electronic mailing lists, source code control systems,
57 and issue tracking systems that are managed by, or on behalf of, the
58 Licensor for the purpose of discussing and improving the Work, but
59 excluding communication that is conspicuously marked or otherwise
60 designated in writing by the copyright owner as "Not a Contribution."
61
62 "Contributor" shall mean Licensor and any individual or Legal Entity
63 on behalf of whom a Contribution has been received by Licensor and
64 subsequently incorporated within the Work.
65
66 2. Grant of Copyright License. Subject to the terms and conditions of
67 this License, each Contributor hereby grants to You a perpetual,
68 worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 copyright license to reproduce, prepare Derivative Works of,
70 publicly display, publicly perform, sublicense, and distribute the
71 Work and such Derivative Works in Source or Object form.
72
73 3. Grant of Patent License. Subject to the terms and conditions of
74 this License, each Contributor hereby grants to You a perpetual,
75 worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 (except as stated in this section) patent license to make, have made,
77 use, offer to sell, sell, import, and otherwise transfer the Work,
78 where such license applies only to those patent claims licensable
79 by such Contributor that are necessarily infringed by their
80 Contribution(s) alone or by combination of their Contribution(s)
81 with the Work to which such Contribution(s) was submitted. If You
82 institute patent litigation against any entity (including a
83 cross-claim or counterclaim in a lawsuit) alleging that the Work
84 or a Contribution incorporated within the Work constitutes direct
85 or contributory patent infringement, then any patent licenses
86 granted to You under this License for that Work shall terminate
87 as of the date such litigation is filed.
88
89 4. Redistribution. You may reproduce and distribute copies of the
90 Work or Derivative Works thereof in any medium, with or without
91 modifications, and in Source or Object form, provided that You
92 meet the following conditions:
93
94 (a) You must give any other recipients of the Work or
95 Derivative Works a copy of this License; and
96
97 (b) You must cause any modified files to carry prominent notices
98 stating that You changed the files; and
99
100 (c) You must retain, in the Source form of any Derivative Works
101 that You distribute, all copyright, patent, trademark, and
102 attribution notices from the Source form of the Work,
103 excluding those notices that do not pertain to any part of
104 the Derivative Works; and
105
106 (d) If the Work includes a "NOTICE" text file as part of its
107 distribution, then any Derivative Works that You distribute must
108 include a readable copy of the attribution notices contained
109 within such NOTICE file, excluding those notices that do not
110 pertain to any part of the Derivative Works, in at least one
111 of the following places: within a NOTICE text file distributed
112 as part of the Derivative Works; within the Source form or
113 documentation, if provided along with the Derivative Works; or,
114 within a display generated by the Derivative Works, if and
115 wherever such third-party notices normally appear. The contents
116 of the NOTICE file are for informational purposes only and
117 do not modify the License. You may add Your own attribution
118 notices within Derivative Works that You distribute, alongside
119 or as an addendum to the NOTICE text from the Work, provided
120 that such additional attribution notices cannot be construed
121 as modifying the License.
122
123 You may add Your own copyright statement to Your modifications and
124 may provide additional or different license terms and conditions
125 for use, reproduction, or distribution of Your modifications, or
126 for any such Derivative Works as a whole, provided Your use,
127 reproduction, and distribution of the Work otherwise complies with
128 the conditions stated in this License.
129
130 5. Submission of Contributions. Unless You explicitly state otherwise,
131 any Contribution intentionally submitted for inclusion in the Work
132 by You to the Licensor shall be under the terms and conditions of
133 this License, without any additional terms or conditions.
134 Notwithstanding the above, nothing herein shall supersede or modify
135 the terms of any separate license agreement you may have executed
136 with Licensor regarding such Contributions.
137
138 6. Trademarks. This License does not grant permission to use the trade
139 names, trademarks, service marks, or product names of the Licensor,
140 except as required for reasonable and customary use in describing the
141 origin of the Work and reproducing the content of the NOTICE file.
142
143 7. Disclaimer of Warranty. Unless required by applicable law or
144 agreed to in writing, Licensor provides the Work (and each
145 Contributor provides its Contributions) on an "AS IS" BASIS,
146 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 implied, including, without limitation, any warranties or conditions
148 of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 PARTICULAR PURPOSE. You are solely responsible for determining the
150 appropriateness of using or redistributing the Work and assume any
151 risks associated with Your exercise of permissions under this License.
152
153 8. Limitation of Liability. In no event and under no legal theory,
154 whether in tort (including negligence), contract, or otherwise,
155 unless required by applicable law (such as deliberate and grossly
156 negligent acts) or agreed to in writing, shall any Contributor be
157 liable to You for damages, including any direct, indirect, special,
158 incidental, or consequential damages of any character arising as a
159 result of this License or out of the use or inability to use the
160 Work (including but not limited to damages for loss of goodwill,
161 work stoppage, computer failure or malfunction, or any and all
162 other commercial damages or losses), even if such Contributor
163 has been advised of the possibility of such damages.
164
165 9. Accepting Warranty or Additional Liability. While redistributing
166 the Work or Derivative Works thereof, You may choose to offer,
167 and charge a fee for, acceptance of support, warranty, indemnity,
168 or other liability obligations and/or rights consistent with this
169 License. However, in accepting such obligations, You may act only
170 on Your own behalf and on Your sole responsibility, not on behalf
171 of any other Contributor, and only if You agree to indemnify,
172 defend, and hold each Contributor harmless for any liability
173 incurred by, or claims asserted against, such Contributor by reason
174 of your accepting any such warranty or additional liability.
175
176 END OF TERMS AND CONDITIONS
177
178 APPENDIX: How to apply the Apache License to your work.
179
180 To apply the Apache License to your work, attach the following
181 boilerplate notice, with the fields enclosed by brackets "[]"
182 replaced with your own identifying information. (Don't include
183 the brackets!) The text should be enclosed in the appropriate
184 comment syntax for the file format. We also recommend that a
185 file or class name and description of purpose be included on the
186 same "printed page" as the copyright notice for easier
187 identification within third-party archives.
188
189 Copyright [yyyy] [name of copyright owner]
190
191 Licensed under the Apache License, Version 2.0 (the "License");
192 you may not use this file except in compliance with the License.
193 You may obtain a copy of the License at
194
195 http://www.apache.org/licenses/LICENSE-2.0
196
197 Unless required by applicable law or agreed to in writing, software
198 distributed under the License is distributed on an "AS IS" BASIS,
199 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 See the License for the specific language governing permissions and
201 limitations under the License.
Added view/app/src/vendor/livekit/LICENSE-components-styles view/app/src/vendor/livekit/LICENSE-components-styles +201 −0 View view/app/src/vendor/livekit/LICENSE-components-styles
@@ -0,0 +1,201 @@
1 Apache License
2 Version 2.0, January 2004
3 http://www.apache.org/licenses/
4
5 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
7 1. Definitions.
8
9 "License" shall mean the terms and conditions for use, reproduction,
10 and distribution as defined by Sections 1 through 9 of this document.
11
12 "Licensor" shall mean the copyright owner or entity authorized by
13 the copyright owner that is granting the License.
14
15 "Legal Entity" shall mean the union of the acting entity and all
16 other entities that control, are controlled by, or are under common
17 control with that entity. For the purposes of this definition,
18 "control" means (i) the power, direct or indirect, to cause the
19 direction or management of such entity, whether by contract or
20 otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 outstanding shares, or (iii) beneficial ownership of such entity.
22
23 "You" (or "Your") shall mean an individual or Legal Entity
24 exercising permissions granted by this License.
25
26 "Source" form shall mean the preferred form for making modifications,
27 including but not limited to software source code, documentation
28 source, and configuration files.
29
30 "Object" form shall mean any form resulting from mechanical
31 transformation or translation of a Source form, including but
32 not limited to compiled object code, generated documentation,
33 and conversions to other media types.
34
35 "Work" shall mean the work of authorship, whether in Source or
36 Object form, made available under the License, as indicated by a
37 copyright notice that is included in or attached to the work
38 (an example is provided in the Appendix below).
39
40 "Derivative Works" shall mean any work, whether in Source or Object
41 form, that is based on (or derived from) the Work and for which the
42 editorial revisions, annotations, elaborations, or other modifications
43 represent, as a whole, an original work of authorship. For the purposes
44 of this License, Derivative Works shall not include works that remain
45 separable from, or merely link (or bind by name) to the interfaces of,
46 the Work and Derivative Works thereof.
47
48 "Contribution" shall mean any work of authorship, including
49 the original version of the Work and any modifications or additions
50 to that Work or Derivative Works thereof, that is intentionally
51 submitted to Licensor for inclusion in the Work by the copyright owner
52 or by an individual or Legal Entity authorized to submit on behalf of
53 the copyright owner. For the purposes of this definition, "submitted"
54 means any form of electronic, verbal, or written communication sent
55 to the Licensor or its representatives, including but not limited to
56 communication on electronic mailing lists, source code control systems,
57 and issue tracking systems that are managed by, or on behalf of, the
58 Licensor for the purpose of discussing and improving the Work, but
59 excluding communication that is conspicuously marked or otherwise
60 designated in writing by the copyright owner as "Not a Contribution."
61
62 "Contributor" shall mean Licensor and any individual or Legal Entity
63 on behalf of whom a Contribution has been received by Licensor and
64 subsequently incorporated within the Work.
65
66 2. Grant of Copyright License. Subject to the terms and conditions of
67 this License, each Contributor hereby grants to You a perpetual,
68 worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 copyright license to reproduce, prepare Derivative Works of,
70 publicly display, publicly perform, sublicense, and distribute the
71 Work and such Derivative Works in Source or Object form.
72
73 3. Grant of Patent License. Subject to the terms and conditions of
74 this License, each Contributor hereby grants to You a perpetual,
75 worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 (except as stated in this section) patent license to make, have made,
77 use, offer to sell, sell, import, and otherwise transfer the Work,
78 where such license applies only to those patent claims licensable
79 by such Contributor that are necessarily infringed by their
80 Contribution(s) alone or by combination of their Contribution(s)
81 with the Work to which such Contribution(s) was submitted. If You
82 institute patent litigation against any entity (including a
83 cross-claim or counterclaim in a lawsuit) alleging that the Work
84 or a Contribution incorporated within the Work constitutes direct
85 or contributory patent infringement, then any patent licenses
86 granted to You under this License for that Work shall terminate
87 as of the date such litigation is filed.
88
89 4. Redistribution. You may reproduce and distribute copies of the
90 Work or Derivative Works thereof in any medium, with or without
91 modifications, and in Source or Object form, provided that You
92 meet the following conditions:
93
94 (a) You must give any other recipients of the Work or
95 Derivative Works a copy of this License; and
96
97 (b) You must cause any modified files to carry prominent notices
98 stating that You changed the files; and
99
100 (c) You must retain, in the Source form of any Derivative Works
101 that You distribute, all copyright, patent, trademark, and
102 attribution notices from the Source form of the Work,
103 excluding those notices that do not pertain to any part of
104 the Derivative Works; and
105
106 (d) If the Work includes a "NOTICE" text file as part of its
107 distribution, then any Derivative Works that You distribute must
108 include a readable copy of the attribution notices contained
109 within such NOTICE file, excluding those notices that do not
110 pertain to any part of the Derivative Works, in at least one
111 of the following places: within a NOTICE text file distributed
112 as part of the Derivative Works; within the Source form or
113 documentation, if provided along with the Derivative Works; or,
114 within a display generated by the Derivative Works, if and
115 wherever such third-party notices normally appear. The contents
116 of the NOTICE file are for informational purposes only and
117 do not modify the License. You may add Your own attribution
118 notices within Derivative Works that You distribute, alongside
119 or as an addendum to the NOTICE text from the Work, provided
120 that such additional attribution notices cannot be construed
121 as modifying the License.
122
123 You may add Your own copyright statement to Your modifications and
124 may provide additional or different license terms and conditions
125 for use, reproduction, or distribution of Your modifications, or
126 for any such Derivative Works as a whole, provided Your use,
127 reproduction, and distribution of the Work otherwise complies with
128 the conditions stated in this License.
129
130 5. Submission of Contributions. Unless You explicitly state otherwise,
131 any Contribution intentionally submitted for inclusion in the Work
132 by You to the Licensor shall be under the terms and conditions of
133 this License, without any additional terms or conditions.
134 Notwithstanding the above, nothing herein shall supersede or modify
135 the terms of any separate license agreement you may have executed
136 with Licensor regarding such Contributions.
137
138 6. Trademarks. This License does not grant permission to use the trade
139 names, trademarks, service marks, or product names of the Licensor,
140 except as required for reasonable and customary use in describing the
141 origin of the Work and reproducing the content of the NOTICE file.
142
143 7. Disclaimer of Warranty. Unless required by applicable law or
144 agreed to in writing, Licensor provides the Work (and each
145 Contributor provides its Contributions) on an "AS IS" BASIS,
146 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 implied, including, without limitation, any warranties or conditions
148 of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 PARTICULAR PURPOSE. You are solely responsible for determining the
150 appropriateness of using or redistributing the Work and assume any
151 risks associated with Your exercise of permissions under this License.
152
153 8. Limitation of Liability. In no event and under no legal theory,
154 whether in tort (including negligence), contract, or otherwise,
155 unless required by applicable law (such as deliberate and grossly
156 negligent acts) or agreed to in writing, shall any Contributor be
157 liable to You for damages, including any direct, indirect, special,
158 incidental, or consequential damages of any character arising as a
159 result of this License or out of the use or inability to use the
160 Work (including but not limited to damages for loss of goodwill,
161 work stoppage, computer failure or malfunction, or any and all
162 other commercial damages or losses), even if such Contributor
163 has been advised of the possibility of such damages.
164
165 9. Accepting Warranty or Additional Liability. While redistributing
166 the Work or Derivative Works thereof, You may choose to offer,
167 and charge a fee for, acceptance of support, warranty, indemnity,
168 or other liability obligations and/or rights consistent with this
169 License. However, in accepting such obligations, You may act only
170 on Your own behalf and on Your sole responsibility, not on behalf
171 of any other Contributor, and only if You agree to indemnify,
172 defend, and hold each Contributor harmless for any liability
173 incurred by, or claims asserted against, such Contributor by reason
174 of your accepting any such warranty or additional liability.
175
176 END OF TERMS AND CONDITIONS
177
178 APPENDIX: How to apply the Apache License to your work.
179
180 To apply the Apache License to your work, attach the following
181 boilerplate notice, with the fields enclosed by brackets "[]"
182 replaced with your own identifying information. (Don't include
183 the brackets!) The text should be enclosed in the appropriate
184 comment syntax for the file format. We also recommend that a
185 file or class name and description of purpose be included on the
186 same "printed page" as the copyright notice for easier
187 identification within third-party archives.
188
189 Copyright [yyyy] [name of copyright owner]
190
191 Licensed under the Apache License, Version 2.0 (the "License");
192 you may not use this file except in compliance with the License.
193 You may obtain a copy of the License at
194
195 http://www.apache.org/licenses/LICENSE-2.0
196
197 Unless required by applicable law or agreed to in writing, software
198 distributed under the License is distributed on an "AS IS" BASIS,
199 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 See the License for the specific language governing permissions and
201 limitations under the License.
Added view/app/src/vendor/livekit/lk-i18n.ts view/app/src/vendor/livekit/lk-i18n.ts +13 −0 View view/app/src/vendor/livekit/lk-i18n.ts
@@ -0,0 +1,13 @@
1/**
2 * Copy for the vendored `@livekit/components-*` sources.
3 *
4 * The official sources ship English literals; every one that can reach the
5 * screen was replaced with an `lkT(key)` call so the dictionaries stay the only
6 * place user-visible copy lives (and so the i18n guard keeps failing on new
7 * hardcoded copy). Keys live in the `livekit` module of the shared dictionaries.
8 */
9import { t, type MessageKey, type MessageParams } from '@app/i18n'
10
11export function lkT(key: MessageKey, params?: MessageParams): string {
12 return t(key, params)
13}
Added view/app/src/vendor/livekit/MANIFEST.json view/app/src/vendor/livekit/MANIFEST.json +1700 −0 View view/app/src/vendor/livekit/MANIFEST.json
@@ -0,0 +1,1700 @@
1{
2 "source": "https://github.com/livekit/components-js",
3 "reason": "Vendored deliberately: the official package names keep working through tsconfig/vite aliases, so this tree can be replaced with a fresh npm tarball and the localModifications below replayed one at a time.",
4 "packages": {
5 "@livekit/components-react": {
6 "version": "2.9.24",
7 "license": "Apache-2.0"
8 },
9 "@livekit/components-core": {
10 "version": "0.12.15",
11 "license": "Apache-2.0"
12 },
13 "@livekit/components-styles": {
14 "version": "1.2.0",
15 "license": "Apache-2.0",
16 "note": "only dist/general (compiled CSS) and dist/types_unprefixed (class-name .scss.d.ts) are vendored"
17 }
18 },
19 "localModifications": [
20 "react/assets/icons/CameraDisabledIcon.tsx",
21 "react/assets/icons/CameraIcon.tsx",
22 "react/assets/icons/ChatCloseIcon.tsx",
23 "react/assets/icons/ChatIcon.tsx",
24 "react/assets/icons/Chevron.tsx",
25 "react/assets/icons/FocusToggleIcon.tsx",
26 "react/assets/icons/GearIcon.tsx",
27 "react/assets/icons/LeaveIcon.tsx",
28 "react/assets/icons/LockLockedIcon.tsx",
29 "react/assets/icons/MicDisabledIcon.tsx",
30 "react/assets/icons/MicIcon.tsx",
31 "react/assets/icons/QualityExcellentIcon.tsx",
32 "react/assets/icons/QualityGoodIcon.tsx",
33 "react/assets/icons/QualityPoorIcon.tsx",
34 "react/assets/icons/QualityUnknownIcon.tsx",
35 "react/assets/icons/ScreenShareIcon.tsx",
36 "react/assets/icons/ScreenShareStopIcon.tsx",
37 "react/assets/icons/SpinnerIcon.tsx",
38 "react/assets/icons/UnfocusToggleIcon.tsx",
39 "react/assets/icons/util.tsx",
40 "react/assets/images/ParticipantPlaceholder.tsx",
41 "react/components/ConnectionStateToast.tsx",
42 "react/components/controls/PaginationControl.tsx",
43 "react/components/controls/StartAudio.tsx",
44 "react/components/controls/StartMediaButton.tsx",
45 "react/components/controls/TrackToggle.tsx",
46 "react/components/participant/ConnectionQualityIndicator.tsx",
47 "react/components/participant/ParticipantTile.tsx",
48 "react/components/RoomAudioRenderer.tsx",
49 "react/components/SessionProvider.tsx",
50 "react/context/session-context.ts",
51 "react/hooks/internal/useResizeObserver.ts",
52 "react/hooks/useAgent.ts",
53 "react/hooks/useEvents.ts",
54 "react/hooks/useGridLayout.ts",
55 "react/hooks/useMediaTrackBySourceOrName.ts",
56 "react/hooks/useParticipantAttributes.ts",
57 "react/hooks/useSession.ts",
58 "react/hooks/useSessionMessages.ts",
59 "react/hooks/useSwipe.ts",
60 "react/hooks/useTrackVolume.ts",
61 "react/hooks/useVoiceAssistant.ts",
62 "react/prefabs/Chat.tsx",
63 "react/prefabs/ControlBar.tsx",
64 "react/prefabs/MediaDeviceMenu.tsx",
65 "react/utils.ts",
66 "core/components/chat.ts",
67 "core/helper/participant-attributes.ts",
68 "core/observables/dataChannel.ts",
69 "core/observables/track.ts"
70 ],
71 "localModificationReasons": {
72 "erasableSyntaxOnly + verbatimModuleSyntax + noUnusedLocals (this repo is stricter than upstream)": [
73 "enums -> as-const objects",
74 "angle-bracket assertions -> as expressions",
75 "type-only imports",
76 "unused React namespace imports removed (jsx: react-jsx)"
77 ],
78 "React 19 / TS 6 / livekit-client 2.22 type drift": [
79 "RefObject<T | null> widening",
80 "useRef initial value",
81 "Uint8Array -> BlobPart cast",
82 "trackSyncTimeObserver media-time payload cast",
83 "process.env guard -> import.meta.env.DEV",
84 "ambient 'events' module declaration (react/types/events.d.ts)"
85 ]
86 },
87 "addedLocally": [
88 "react/types/events.d.ts"
89 ],
90 "removedFromUpstream": [
91 "react/assets/template.js",
92 "react/hooks/cloud/krisp/useKrispNoiseFilter.ts",
93 "react/hooks/useAgentExpression.test.ts",
94 "react/hooks/useMediaDeviceSelect.test.ts",
95 "react/hooks/usePagination.test.ts",
96 "react/hooks/useTracks.test.ts",
97 "react/index.docs.ts",
98 "core/helper/emailRegex.test.ts",
99 "core/helper/grid-layouts.test.ts",
100 "core/helper/url-regex.test.ts",
101 "core/sorting/base-sort-functions.test.ts",
102 "core/sorting/sort-track-bundles.test.ts",
103 "core/sorting/tile-array-update.test.ts",
104 "core/track-reference/test-utils.test.ts",
105 "core/track-reference/track-reference.utils.test.ts",
106 "core/utilis.test.ts",
107 "styles/dist/types/general/components/controls/index.css.d.ts",
108 "styles/dist/types/general/components/index.css.d.ts",
109 "styles/dist/types/general/components/layout/index.css.d.ts",
110 "styles/dist/types/general/components/participant/index.css.d.ts",
111 "styles/dist/types/general/index.css.d.ts",
112 "styles/dist/types/general/prefabs/audio-conference.css.d.ts",
113 "styles/dist/types/general/prefabs/chat.css.d.ts",
114 "styles/dist/types/general/prefabs/control-bar.css.d.ts",
115 "styles/dist/types/general/prefabs/index.css.d.ts",
116 "styles/dist/types/general/prefabs/prejoin.css.d.ts",
117 "styles/dist/types/general/prefabs/video-conference.css.d.ts",
118 "styles/dist/types/general/themes/huddle.css.d.ts"
119 ],
120 "files": [
121 {
122 "path": "react/assets/icons/CameraDisabledIcon.tsx",
123 "package": "react",
124 "sha256": "c039bf27027a85bf5ba5677d16fe2362133ac3bb1c6a180269c18b500a8470e8",
125 "upstreamSha256": "6df862d79b0bdd70866667212562af3e187fae6c13a1f989636ba3361cfab66c"
126 },
127 {
128 "path": "react/assets/icons/CameraIcon.tsx",
129 "package": "react",
130 "sha256": "25a0a6654982d09a700d6ab4c71d98eb353be74567aef2d7cad8437946b2da4d",
131 "upstreamSha256": "1558194d31ca53d6c21f3e1b4a7425859d9b7f39b560299baff0b9864e4de6fa"
132 },
133 {
134 "path": "react/assets/icons/ChatCloseIcon.tsx",
135 "package": "react",
136 "sha256": "d759011a2f416fbae58541eec7d72f28008730942f0b896806baacb7893b0209",
137 "upstreamSha256": "3379d0f223ac8868561bb14ffcb72efd7ad3eb7bf5b640f0cb4181ce6c897a33"
138 },
139 {
140 "path": "react/assets/icons/ChatIcon.tsx",
141 "package": "react",
142 "sha256": "af79e2b4f006511c834c4202443b5a003b178571f76677a3e177eab88cdb0fce",
143 "upstreamSha256": "5f328af7402379fcd5968bda677544cb9c1e16d4606c36ed5b2b1baf04e14f41"
144 },
145 {
146 "path": "react/assets/icons/Chevron.tsx",
147 "package": "react",
148 "sha256": "2ce05679f570734b88be39364dfd163ab3a5269493f4023c66b8df54ee096674",
149 "upstreamSha256": "3be28e41c65165daeeaaea8555673a0a2a1335a305439637b51b1988447a87ea"
150 },
151 {
152 "path": "react/assets/icons/FocusToggleIcon.tsx",
153 "package": "react",
154 "sha256": "5213be1136ae3d11f5c0821e5897ebe3d68fe81be8ec7ead8b0bbd5d0df71a78",
155 "upstreamSha256": "6668539bfecfca11356dfb5024b24a08e595b1cda59ded5f500e9aadda0ff813"
156 },
157 {
158 "path": "react/assets/icons/GearIcon.tsx",
159 "package": "react",
160 "sha256": "28eadd62d6a046bf1a4fa5a22325443aca60bd37528747c6dd22c6995f29f5e2",
161 "upstreamSha256": "b5504f65c8e1a7f361ba2d2abb7e2a20a2797e5a65cb2285519ff4406d0544f6"
162 },
163 {
164 "path": "react/assets/icons/index.ts",
165 "package": "react",
166 "sha256": "ec1e3c5f0d648767551eb68af7104f3413c3a9f693891dbd271fc4acb229c579",
167 "upstreamSha256": "ec1e3c5f0d648767551eb68af7104f3413c3a9f693891dbd271fc4acb229c579"
168 },
169 {
170 "path": "react/assets/icons/LeaveIcon.tsx",
171 "package": "react",
172 "sha256": "d754f922f6fa6f4d18e2bca0ff493c382eef772ba0e966e5c94c437cb892d5db",
173 "upstreamSha256": "d561fcbde561aada1047fa85b7ada93112d3bd396a2c89b343fbb0c17964a728"
174 },
175 {
176 "path": "react/assets/icons/LockLockedIcon.tsx",
177 "package": "react",
178 "sha256": "9b8a3a66f6e43a7f6557035778cce731beaf2b625febc6c4f145f5fb84a0ae35",
179 "upstreamSha256": "851ce4371ad582d1671713130e92b4ff30594cea0cf191fbbd086ebd1accceab"
180 },
181 {
182 "path": "react/assets/icons/MicDisabledIcon.tsx",
183 "package": "react",
184 "sha256": "5d89976e706efce7aca906f3d965be5a14e9c9e3c6d9a233c243bfed0f42245d",
185 "upstreamSha256": "4a77e4ce816476ef38490b1ae5d678c573a28ab37ee3272fb4c68fb05e5466d9"
186 },
187 {
188 "path": "react/assets/icons/MicIcon.tsx",
189 "package": "react",
190 "sha256": "ae85bd6bb4c7a3a9adfa3430ff1fdbeff89a402fe03b5e1fd73e1b355edcc1ed",
191 "upstreamSha256": "2bce39313fc511358728be02fba33475a396672db7b19416dd2c8f1eeeea00f0"
192 },
193 {
194 "path": "react/assets/icons/QualityExcellentIcon.tsx",
195 "package": "react",
196 "sha256": "821f869d93257b95e23947a6b7fd5ae8459c3969f92bf35baef2a45a0df638b5",
197 "upstreamSha256": "5f899a60465a3128d906f33b53da01f84d91c88bf195b05b91d222fb2ddb44e6"
198 },
199 {
200 "path": "react/assets/icons/QualityGoodIcon.tsx",
201 "package": "react",
202 "sha256": "fb784e11af065a39cddfabf7cdf9d023287142507dc1853d2ba5fac026ebcbe5",
203 "upstreamSha256": "c07b02baca1e2094672cd12363f4830c7236f883822fd15efbdb417893e701fe"
204 },
205 {
206 "path": "react/assets/icons/QualityPoorIcon.tsx",
207 "package": "react",
208 "sha256": "039eaeee8c80b991491627fde0ee2b52cc8974a86feac25416232448d7d89786",
209 "upstreamSha256": "9873f559d03ef9ea7434edb3b51bb16684e928edad8a44e5ee438ff52ef91e26"
210 },
211 {
212 "path": "react/assets/icons/QualityUnknownIcon.tsx",
213 "package": "react",
214 "sha256": "a2668b149f0875dba291f8879704c85489ed492e1bfc8228109ad773ce4b45e1",
215 "upstreamSha256": "9aac81fd8adac7956cdeb9e433ad55ff4994e04ec6e201bf869739fc75597357"
216 },
217 {
218 "path": "react/assets/icons/ScreenShareIcon.tsx",
219 "package": "react",
220 "sha256": "3f0f7a79eb4b3176a525c78587fc3fed75257e15acffd15974652610eeda016f",
221 "upstreamSha256": "50ceab6f3f8ec905aff5e1ebb8a2c6e09a2c16e7011f937c82e2369843e8c02b"
222 },
223 {
224 "path": "react/assets/icons/ScreenShareStopIcon.tsx",
225 "package": "react",
226 "sha256": "cf1e4948855c297b0fac7190538069ffb4fae17d2976f2ca3ce2d385ac6c822d",
227 "upstreamSha256": "d0b895ad71aa751244247f097dedb46799533d87d82d67e6b4ff9d6b6ea9407d"
228 },
229 {
230 "path": "react/assets/icons/SpinnerIcon.tsx",
231 "package": "react",
232 "sha256": "dd189b6f6a4bde8db0ccea8ed7db273b6756d0a82fe0fbda602318f89340c92a",
233 "upstreamSha256": "1be7288eb1e0331db2ec1dbc77de584375e401841a629720678fc56e18d6cc7d"
234 },
235 {
236 "path": "react/assets/icons/UnfocusToggleIcon.tsx",
237 "package": "react",
238 "sha256": "6f0bd932ab4bfda092902a5b1bf690b609d0fbf4d1220ec19f4c0d714b075d00",
239 "upstreamSha256": "00d94585858042d5219b1cd8af1435b054f9fc9a54f3972ae07a775793108476"
240 },
241 {
242 "path": "react/assets/icons/util.tsx",
243 "package": "react",
244 "sha256": "55fa895f70109258d0005dcead9f65a39813252f31b8aefc56f5a9109e6b1c83",
245 "upstreamSha256": "57eed846c8f39ff360df9ebd7e01bde7fb03b49c42f0cd595dcd46584d0624f4"
246 },
247 {
248 "path": "react/assets/images/index.ts",
249 "package": "react",
250 "sha256": "d8cd5b3855a83255827feba961816678e633f0348dafa6108195fe8d038ada7c",
251 "upstreamSha256": "d8cd5b3855a83255827feba961816678e633f0348dafa6108195fe8d038ada7c"
252 },
253 {
254 "path": "react/assets/images/ParticipantPlaceholder.tsx",
255 "package": "react",
256 "sha256": "a29b7c5464933b6092daecbfe74c1cc35ea6d340b49f31d61b61ad8eb200c9d6",
257 "upstreamSha256": "c1d621fbc53c654099344d4e1a0ea97b43f9a69dc679506503c44aaec9198806"
258 },
259 {
260 "path": "react/components/ChatEntry.tsx",
261 "package": "react",
262 "sha256": "8900973b0a30444489749a35381f77b58df6c7ebea47f963e6e31f21fc3ec427",
263 "upstreamSha256": "8900973b0a30444489749a35381f77b58df6c7ebea47f963e6e31f21fc3ec427"
264 },
265 {
266 "path": "react/components/ConnectionState.tsx",
267 "package": "react",
268 "sha256": "d865948957b59d359867e9fc07f2f10456efa0238927a1e75193b69c6f99573c",
269 "upstreamSha256": "d865948957b59d359867e9fc07f2f10456efa0238927a1e75193b69c6f99573c"
270 },
271 {
272 "path": "react/components/ConnectionStateToast.tsx",
273 "package": "react",
274 "sha256": "0a231ea23684a5813e468e146e259f9fb22578d0477cf76d84f17c5da7ab01fe",
275 "upstreamSha256": "ece68ef47403e6258d83a5049a60162f96777c5004bd5d6841f58e86ce153777"
276 },
277 {
278 "path": "react/components/controls/ChatToggle.tsx",
279 "package": "react",
280 "sha256": "46add22c36793e1df57cabf24e718461c4dccd078924aa73cec22c9e6cb26682",
281 "upstreamSha256": "46add22c36793e1df57cabf24e718461c4dccd078924aa73cec22c9e6cb26682"
282 },
283 {
284 "path": "react/components/controls/ClearPinButton.tsx",
285 "package": "react",
286 "sha256": "78223108ac702c408de74d9e7ab5f8c69f2bbb055fcefb38d69afde370a7f6aa",
287 "upstreamSha256": "78223108ac702c408de74d9e7ab5f8c69f2bbb055fcefb38d69afde370a7f6aa"
288 },
289 {
290 "path": "react/components/controls/DisconnectButton.tsx",
291 "package": "react",
292 "sha256": "3e606eb3dc6f82d686675591a5af972d9bf1f6ec21562602906a8439ccff5b4a",
293 "upstreamSha256": "3e606eb3dc6f82d686675591a5af972d9bf1f6ec21562602906a8439ccff5b4a"
294 },
295 {
296 "path": "react/components/controls/FocusToggle.tsx",
297 "package": "react",
298 "sha256": "6932348d883b7116c568ebe52f682abe485d32efe69eb9a6b2e833da519d7fbd",
299 "upstreamSha256": "6932348d883b7116c568ebe52f682abe485d32efe69eb9a6b2e833da519d7fbd"
300 },
301 {
302 "path": "react/components/controls/MediaDeviceSelect.tsx",
303 "package": "react",
304 "sha256": "c7f1ac4972eab877f313f6b74b3ba47cf2bee7a45df5b3423e085c32f471270c",
305 "upstreamSha256": "c7f1ac4972eab877f313f6b74b3ba47cf2bee7a45df5b3423e085c32f471270c"
306 },
307 {
308 "path": "react/components/controls/PaginationControl.tsx",
309 "package": "react",
310 "sha256": "efce49bb434b846a1f42eaab4bd8ecd986f34e0ca27a4fca601467d788641cb1",
311 "upstreamSha256": "5042baefa132f7a308033a1a4c1a31a029ea15bf82e51792d70acc73e60de8ef"
312 },
313 {
314 "path": "react/components/controls/PaginationIndicator.tsx",
315 "package": "react",
316 "sha256": "32ffc1dc4da0d58deaf93c96d4c8446975e08d8f801ca0c60fed35f1438f443e",
317 "upstreamSha256": "32ffc1dc4da0d58deaf93c96d4c8446975e08d8f801ca0c60fed35f1438f443e"
318 },
319 {
320 "path": "react/components/controls/SettingsMenuToggle.tsx",
321 "package": "react",
322 "sha256": "f183161ae4e170f0276c7470bcb32ac62d37d3753d368da8f15142179f9a49e2",
323 "upstreamSha256": "f183161ae4e170f0276c7470bcb32ac62d37d3753d368da8f15142179f9a49e2"
324 },
325 {
326 "path": "react/components/controls/StartAudio.tsx",
327 "package": "react",
328 "sha256": "c6abbeae18ab3707fabfde64a4bcc8324badbf91c724e9b5a2204bace093f2a7",
329 "upstreamSha256": "f93845a26b16cc2863f9a1f50b6ad4dea5b606a06779fdae89f1590496c52bab"
330 },
331 {
332 "path": "react/components/controls/StartMediaButton.tsx",
333 "package": "react",
334 "sha256": "e0705441706f385a4ab97640ea55a4cb87d08f4d63bccf2b63595a9250c1b298",
335 "upstreamSha256": "f2735fdce1a907908d6dbfe964ce51a5759c69a7f2e913bbd0c195911710642f"
336 },
337 {
338 "path": "react/components/controls/TrackToggle.tsx",
339 "package": "react",
340 "sha256": "afc6f84d27235563f4518e90fcc8a14f2e61e5a6e1d94a44a30a8c72c5255d37",
341 "upstreamSha256": "4c306e53b3708a4f9e5d916746ca50754ec7af745b174e6e9002f9c1b77317d3"
342 },
343 {
344 "path": "react/components/index.ts",
345 "package": "react",
346 "sha256": "5b4c7be254d37b5ce2f993ccf843fc3e6a5298a4560ed940332fb3773f09a26e",
347 "upstreamSha256": "5b4c7be254d37b5ce2f993ccf843fc3e6a5298a4560ed940332fb3773f09a26e"
348 },
349 {
350 "path": "react/components/layout/CarouselLayout.tsx",
351 "package": "react",
352 "sha256": "b8141c5958e14c003ab7bf735f42cda21f341fc65ee94c043da2f902554460e5",
353 "upstreamSha256": "b8141c5958e14c003ab7bf735f42cda21f341fc65ee94c043da2f902554460e5"
354 },
355 {
356 "path": "react/components/layout/FocusLayout.tsx",
357 "package": "react",
358 "sha256": "9bb4724173c3d976af7f9bcc36ea52629ca09e912e00fac9765b5b4d14f0bf3a",
359 "upstreamSha256": "9bb4724173c3d976af7f9bcc36ea52629ca09e912e00fac9765b5b4d14f0bf3a"
360 },
361 {
362 "path": "react/components/layout/GridLayout.tsx",
363 "package": "react",
364 "sha256": "7b2f0169b00d373f5004bb0de46909cb123657be427225faccfcd770bd66f069",
365 "upstreamSha256": "7b2f0169b00d373f5004bb0de46909cb123657be427225faccfcd770bd66f069"
366 },
367 {
368 "path": "react/components/layout/index.ts",
369 "package": "react",
370 "sha256": "a2b2ce92709ed92a8e3475f4ae19804efaf9a832d4bc0f5bc32867a8122ae5ce",
371 "upstreamSha256": "a2b2ce92709ed92a8e3475f4ae19804efaf9a832d4bc0f5bc32867a8122ae5ce"
372 },
373 {
374 "path": "react/components/layout/LayoutContextProvider.tsx",
375 "package": "react",
376 "sha256": "9e699d6454c8803d06c5080216184bbe4085c89ae7a01d96c61fe5032cc1a44a",
377 "upstreamSha256": "9e699d6454c8803d06c5080216184bbe4085c89ae7a01d96c61fe5032cc1a44a"
378 },
379 {
380 "path": "react/components/LiveKitRoom.tsx",
381 "package": "react",
382 "sha256": "1cfe5af7b41d26934b02cf842e2fe98ea5529a07cdef49961c874ca3aec3fa75",
383 "upstreamSha256": "1cfe5af7b41d26934b02cf842e2fe98ea5529a07cdef49961c874ca3aec3fa75"
384 },
385 {
386 "path": "react/components/participant/animationSequences/connectingSequence.ts",
387 "package": "react",
388 "sha256": "f6909ad81bd9f6f02e394c80705a3704bf304ac3ac052250e7b9f9d95859d973",
389 "upstreamSha256": "f6909ad81bd9f6f02e394c80705a3704bf304ac3ac052250e7b9f9d95859d973"
390 },
391 {
392 "path": "react/components/participant/animationSequences/listeningSequence.ts",
393 "package": "react",
394 "sha256": "a9715388daf98b9ef0bf60a67a9100088219deab3c57055cce5475b5fb544394",
395 "upstreamSha256": "a9715388daf98b9ef0bf60a67a9100088219deab3c57055cce5475b5fb544394"
396 },
397 {
398 "path": "react/components/participant/animationSequences/thinkingSequence.ts",
399 "package": "react",
400 "sha256": "1aaafee4c164f46c3ddde1086b5e9399264d2421efcd55179d567c1458c16a01",
401 "upstreamSha256": "1aaafee4c164f46c3ddde1086b5e9399264d2421efcd55179d567c1458c16a01"
402 },
403 {
404 "path": "react/components/participant/animators/useBarAnimator.ts",
405 "package": "react",
406 "sha256": "1172ce1df1e6dc2638a67b71e9af86a8ec4181ce75b092f33b30aa53292abc90",
407 "upstreamSha256": "1172ce1df1e6dc2638a67b71e9af86a8ec4181ce75b092f33b30aa53292abc90"
408 },
409 {
410 "path": "react/components/participant/AudioTrack.tsx",
411 "package": "react",
412 "sha256": "765fc0bf3e0de5cfd19ecbf3ad97960632ff8afab2f762231773b1a2c334d40d",
413 "upstreamSha256": "765fc0bf3e0de5cfd19ecbf3ad97960632ff8afab2f762231773b1a2c334d40d"
414 },
415 {
416 "path": "react/components/participant/AudioVisualizer.tsx",
417 "package": "react",
418 "sha256": "cc144cdb683ca7f17b85321ab7adb2b8298390a016f36d7c0d8f6b94f76e0cba",
419 "upstreamSha256": "cc144cdb683ca7f17b85321ab7adb2b8298390a016f36d7c0d8f6b94f76e0cba"
420 },
421 {
422 "path": "react/components/participant/BarVisualizer.tsx",
423 "package": "react",
424 "sha256": "8b0f04b6889607acdefd3727e4681455fb27e8af68f1130db45d4d9847e89df6",
425 "upstreamSha256": "8b0f04b6889607acdefd3727e4681455fb27e8af68f1130db45d4d9847e89df6"
426 },
427 {
428 "path": "react/components/participant/ConnectionQualityIndicator.tsx",
429 "package": "react",
430 "sha256": "5ddb8359376bf1025f9d63c5cabb63a0b4a3645a9f858b8a896a5bcef97b6ea9",
431 "upstreamSha256": "2a2b0fabce9923b41f5f1ee996b2c9c9d53ea476e0705859e7877bd30c8a92ac"
432 },
433 {
434 "path": "react/components/participant/ParticipantAudioTile.tsx",
435 "package": "react",
436 "sha256": "284ae473b480ec75ed76bfbbc07f86d757a76b24abc81bc73c26d3b837c710cd",
437 "upstreamSha256": "284ae473b480ec75ed76bfbbc07f86d757a76b24abc81bc73c26d3b837c710cd"
438 },
439 {
440 "path": "react/components/participant/ParticipantName.tsx",
441 "package": "react",
442 "sha256": "6570dfc4e6019de067860a0750276f121182a8e77b6b99650c27d423b60778e0",
443 "upstreamSha256": "6570dfc4e6019de067860a0750276f121182a8e77b6b99650c27d423b60778e0"
444 },
445 {
446 "path": "react/components/participant/ParticipantTile.tsx",
447 "package": "react",
448 "sha256": "f386abfa08607d2dc4e3d522b254685f5c51100d2ae45af985c110628c2aff58",
449 "upstreamSha256": "00209b0eee509769c1575cc63b7b70f6e286974e571ccff7982920dc8368953e"
450 },
451 {
452 "path": "react/components/participant/TrackMutedIndicator.tsx",
453 "package": "react",
454 "sha256": "8a2a55c0b6c002ed082ae1077058bcaa33a60d62eb57b62bd60904233798e401",
455 "upstreamSha256": "8a2a55c0b6c002ed082ae1077058bcaa33a60d62eb57b62bd60904233798e401"
456 },
457 {
458 "path": "react/components/participant/VideoTrack.tsx",
459 "package": "react",
460 "sha256": "526f8893fde547bf48c91b71a43cf4e16b7d59ed1a99646a77ead0d9671abb54",
461 "upstreamSha256": "526f8893fde547bf48c91b71a43cf4e16b7d59ed1a99646a77ead0d9671abb54"
462 },
463 {
464 "path": "react/components/ParticipantLoop.tsx",
465 "package": "react",
466 "sha256": "43315c19c73d4c939b549ea165fcdf85d4f282a6a5f9dfb573ca293ebe382d5d",
467 "upstreamSha256": "43315c19c73d4c939b549ea165fcdf85d4f282a6a5f9dfb573ca293ebe382d5d"
468 },
469 {
470 "path": "react/components/RoomAudioRenderer.tsx",
471 "package": "react",
472 "sha256": "8b1b3e54c68f8bde6bde6af6094f9ade3ad877000a6ac0c2e34c90866a5dda13",
473 "upstreamSha256": "69023c2dcecbea5b6b432d54f55d6472ae1cd9eda321d3f91bcd645cd8068308"
474 },
475 {
476 "path": "react/components/RoomName.tsx",
477 "package": "react",
478 "sha256": "c866799f0b3d2313c31dbac833f114ff5aac8d5db06ef51e47794fe2a388b279",
479 "upstreamSha256": "c866799f0b3d2313c31dbac833f114ff5aac8d5db06ef51e47794fe2a388b279"
480 },
481 {
482 "path": "react/components/SessionProvider.tsx",
483 "package": "react",
484 "sha256": "1b3af2db254a9411987a4227f17a5d405782154c6a815870bb7e747b451998fc",
485 "upstreamSha256": "d8494e4dc228ddfa0e7b5a5bbed2764ac306a601e94d80c8db6eec4efc0bafb7"
486 },
487 {
488 "path": "react/components/Toast.tsx",
489 "package": "react",
490 "sha256": "f8407b7d222225b2c3ab31dbb0be0a16e21c0e82b20784bebaba31bf1e1622f5",
491 "upstreamSha256": "f8407b7d222225b2c3ab31dbb0be0a16e21c0e82b20784bebaba31bf1e1622f5"
492 },
493 {
494 "path": "react/components/TrackLoop.tsx",
495 "package": "react",
496 "sha256": "06843d6be4f6645c149022f3cdd2e95a7f115d2d77d16f5a8b0da683e87c1876",
497 "upstreamSha256": "06843d6be4f6645c149022f3cdd2e95a7f115d2d77d16f5a8b0da683e87c1876"
498 },
499 {
500 "path": "react/context/chat-context.ts",
501 "package": "react",
502 "sha256": "df104b33b1d0f5d91c9c2d2b4bc893ed4c17eeedc8b225098bd92872d058fab3",
503 "upstreamSha256": "df104b33b1d0f5d91c9c2d2b4bc893ed4c17eeedc8b225098bd92872d058fab3"
504 },
505 {
506 "path": "react/context/feature-context.ts",
507 "package": "react",
508 "sha256": "0e60c34ddb7f2f2021e5f164e22596733d546456cf9a1fa19e991cef14dbb23a",
509 "upstreamSha256": "0e60c34ddb7f2f2021e5f164e22596733d546456cf9a1fa19e991cef14dbb23a"
510 },
511 {
512 "path": "react/context/index.ts",
513 "package": "react",
514 "sha256": "99a3dafc369ba35f72500ddcbc284c05674102b0fcd17768654d50e09db0d701",
515 "upstreamSha256": "99a3dafc369ba35f72500ddcbc284c05674102b0fcd17768654d50e09db0d701"
516 },
517 {
518 "path": "react/context/layout-context.ts",
519 "package": "react",
520 "sha256": "baa46009c70f116e3c99822d5e5e39a069be725b66395ff151487d5f860c801a",
521 "upstreamSha256": "baa46009c70f116e3c99822d5e5e39a069be725b66395ff151487d5f860c801a"
522 },
523 {
524 "path": "react/context/participant-context.ts",
525 "package": "react",
526 "sha256": "4b9b98c04c73948a24d2c93c19cf0f9169763de4f854c49423cae6e9cde126e5",
527 "upstreamSha256": "4b9b98c04c73948a24d2c93c19cf0f9169763de4f854c49423cae6e9cde126e5"
528 },
529 {
530 "path": "react/context/pin-context.ts",
531 "package": "react",
532 "sha256": "bae29726b275b2c5fea49ad6a09c225e6a02396b9644d098cd99f35467ab9e28",
533 "upstreamSha256": "bae29726b275b2c5fea49ad6a09c225e6a02396b9644d098cd99f35467ab9e28"
534 },
535 {
536 "path": "react/context/room-context.ts",
537 "package": "react",
538 "sha256": "c45b4a30513058f79a89db31d5940464194ebd96491c83090aa8e5d0c4dea7a7",
539 "upstreamSha256": "c45b4a30513058f79a89db31d5940464194ebd96491c83090aa8e5d0c4dea7a7"
540 },
541 {
542 "path": "react/context/session-context.ts",
543 "package": "react",
544 "sha256": "998e174f2e2024b4e98d7e9a88ecee41e5bcad43bb1d7b033fdba1aafa37d98b",
545 "upstreamSha256": "de7f1a3f2c2f139074728527c791868b26c9a25928e820d6d9ef61c60e27db49"
546 },
547 {
548 "path": "react/context/track-reference-context.ts",
549 "package": "react",
550 "sha256": "e1f9eb8a73f92b7cef2d8e45ccbfff5f0b094b05c16ac0d400d13da55128684a",
551 "upstreamSha256": "e1f9eb8a73f92b7cef2d8e45ccbfff5f0b094b05c16ac0d400d13da55128684a"
552 },
553 {
554 "path": "react/hooks/index.ts",
555 "package": "react",
556 "sha256": "0b522f92911ba46db6d08d73bb3b8af1b25f8441b064fba121f9f113af33f5ae",
557 "upstreamSha256": "0b522f92911ba46db6d08d73bb3b8af1b25f8441b064fba121f9f113af33f5ae"
558 },
559 {
560 "path": "react/hooks/internal/index.ts",
561 "package": "react",
562 "sha256": "7163f755210cdd2c1fc051f8d2af8fa08a600b919ae6a59b41dac4116786eb3d",
563 "upstreamSha256": "7163f755210cdd2c1fc051f8d2af8fa08a600b919ae6a59b41dac4116786eb3d"
564 },
565 {
566 "path": "react/hooks/internal/useMediaQuery.ts",
567 "package": "react",
568 "sha256": "1c6691f33194f3156b36bf2bb81583d92edf42ddcf2fdb1a67aa25c5c3921dcd",
569 "upstreamSha256": "1c6691f33194f3156b36bf2bb81583d92edf42ddcf2fdb1a67aa25c5c3921dcd"
570 },
571 {
572 "path": "react/hooks/internal/useObservableState.ts",
573 "package": "react",
574 "sha256": "8caab14ec413944f36ed78e0b01ef056ab8cb4882e39bcca0376969ceee0b86c",
575 "upstreamSha256": "8caab14ec413944f36ed78e0b01ef056ab8cb4882e39bcca0376969ceee0b86c"
576 },
577 {
578 "path": "react/hooks/internal/useResizeObserver.ts",
579 "package": "react",
580 "sha256": "2290b6857672e56945463eb7dd121f11df2985d9dad85efbd03ae4962da7b79f",
581 "upstreamSha256": "79f4254013def84c913e3abad96b1f2ea70f26ec9e19cb81147064c42a01a0a7"
582 },
583 {
584 "path": "react/hooks/useAgent.ts",
585 "package": "react",
586 "sha256": "ccb06915e7c4feabd9295e9b438b898f08c1224fa6ef40523ae1e712b5fe6c62",
587 "upstreamSha256": "7a394d9923bf1d447b9dba239b2c2237fd50246b39a49b5ee5a18a4de85ed7b9"
588 },
589 {
590 "path": "react/hooks/useAgentExpression.ts",
591 "package": "react",
592 "sha256": "eea3752e144ada7bc0d7cd085a71eebb99dc89ad49388d989e80da434aba3c0a",
593 "upstreamSha256": "eea3752e144ada7bc0d7cd085a71eebb99dc89ad49388d989e80da434aba3c0a"
594 },
595 {
596 "path": "react/hooks/useAudioPlayback.ts",
597 "package": "react",
598 "sha256": "9069f52cbf4372a1a1704609d2e3eecbd11ddd677994a997635529b139a2da4a",
599 "upstreamSha256": "9069f52cbf4372a1a1704609d2e3eecbd11ddd677994a997635529b139a2da4a"
600 },
601 {
602 "path": "react/hooks/useChat.ts",
603 "package": "react",
604 "sha256": "f14c4c61480b78f430ba365582717a212cc38c06b2a34eac724f3175ce0b2f51",
605 "upstreamSha256": "f14c4c61480b78f430ba365582717a212cc38c06b2a34eac724f3175ce0b2f51"
606 },
607 {
608 "path": "react/hooks/useChatToggle.ts",
609 "package": "react",
610 "sha256": "4dd405b9dc11746d83126087322d0c1483c607f56c9c797cb6bb704ab22bf28e",
611 "upstreamSha256": "4dd405b9dc11746d83126087322d0c1483c607f56c9c797cb6bb704ab22bf28e"
612 },
613 {
614 "path": "react/hooks/useClearPinButton.ts",
615 "package": "react",
616 "sha256": "683c5e9e47253d6ad6bf54e66ecf6a19b122aa3dcef0f5f2598e1c839a03859f",
617 "upstreamSha256": "683c5e9e47253d6ad6bf54e66ecf6a19b122aa3dcef0f5f2598e1c839a03859f"
618 },
619 {
620 "path": "react/hooks/useConnectionQualityIndicator.ts",
621 "package": "react",
622 "sha256": "102e8fa23f666b8d7e49dcfdf580ee0055b68e4cf246e7d18907b5fb89003f74",
623 "upstreamSha256": "102e8fa23f666b8d7e49dcfdf580ee0055b68e4cf246e7d18907b5fb89003f74"
624 },
625 {
626 "path": "react/hooks/useConnectionStatus.ts",
627 "package": "react",
628 "sha256": "8b78a34a662bc129919e0a77ed845ff5d66aa5faafb6f8a448b4c66c1c6ca1b5",
629 "upstreamSha256": "8b78a34a662bc129919e0a77ed845ff5d66aa5faafb6f8a448b4c66c1c6ca1b5"
630 },
631 {
632 "path": "react/hooks/useDataChannel.ts",
633 "package": "react",
634 "sha256": "28e88ed5e971634726c202e0397aa8156e1da8d7445c579a10ef4be6f6c57365",
635 "upstreamSha256": "28e88ed5e971634726c202e0397aa8156e1da8d7445c579a10ef4be6f6c57365"
636 },
637 {
638 "path": "react/hooks/useDisconnectButton.ts",
639 "package": "react",
640 "sha256": "0969fe0327ce99f405e5f76a6326dcf0c3ddf3bfc0385cad05ff275160ab0da3",
641 "upstreamSha256": "0969fe0327ce99f405e5f76a6326dcf0c3ddf3bfc0385cad05ff275160ab0da3"
642 },
643 {
644 "path": "react/hooks/useEvents.ts",
645 "package": "react",
646 "sha256": "6c8b9e956b0d46671167d2ff76e8a4e5c1fbff0dad7dec6781ceaef127be6f8a",
647 "upstreamSha256": "6ec1a9703231b1b39061c81435f890d1ab93776c8d241c1387fb72f600c384ec"
648 },
649 {
650 "path": "react/hooks/useFacingMode.ts",
651 "package": "react",
652 "sha256": "11c4670b0846570153490808244d627955737b08b75474d49ce2d5824ab48548",
653 "upstreamSha256": "11c4670b0846570153490808244d627955737b08b75474d49ce2d5824ab48548"
654 },
655 {
656 "path": "react/hooks/useFocusToggle.ts",
657 "package": "react",
658 "sha256": "0c52a4ae7c2f6b0e5ac769fa33c4b206b2863bb90419214c7e7fb5d5013b2bf1",
659 "upstreamSha256": "0c52a4ae7c2f6b0e5ac769fa33c4b206b2863bb90419214c7e7fb5d5013b2bf1"
660 },
661 {
662 "path": "react/hooks/useGridLayout.ts",
663 "package": "react",
664 "sha256": "414f9b9b5b3d66c5bf92707845808d6aa927f7356ea6e101e80f479fb738e0e6",
665 "upstreamSha256": "41fe485e1e2ba912f03be117cd21c7e873aa1a3de9999a0e862e09c3e573d523"
666 },
667 {
668 "path": "react/hooks/useIsEncrypted.ts",
669 "package": "react",
670 "sha256": "c240b54598770a218d0eb106926d75b73cf87e864621f73305969e3286d9841b",
671 "upstreamSha256": "c240b54598770a218d0eb106926d75b73cf87e864621f73305969e3286d9841b"
672 },
673 {
674 "path": "react/hooks/useIsMuted.ts",
675 "package": "react",
676 "sha256": "a8fa40d0a13a954d0e0066c98ce451dafee51adb30b27ffae7cc29d8157832f8",
677 "upstreamSha256": "a8fa40d0a13a954d0e0066c98ce451dafee51adb30b27ffae7cc29d8157832f8"
678 },
679 {
680 "path": "react/hooks/useIsRecording.ts",
681 "package": "react",
682 "sha256": "8ff5016d5b52fea2afaa028c82a2a4417c923aac5b31f4a7cb2bf900c176e560",
683 "upstreamSha256": "8ff5016d5b52fea2afaa028c82a2a4417c923aac5b31f4a7cb2bf900c176e560"
684 },
685 {
686 "path": "react/hooks/useIsSpeaking.ts",
687 "package": "react",
688 "sha256": "550ea0352b8147419f32c00aaf66d0fb99d4c67a3a6b85f49a252500542fc425",
689 "upstreamSha256": "550ea0352b8147419f32c00aaf66d0fb99d4c67a3a6b85f49a252500542fc425"
690 },
691 {
692 "path": "react/hooks/useLiveKitRoom.ts",
693 "package": "react",
694 "sha256": "ed157ab34cb8dfaa25870ec24c116967a7d77e085ab4e5800ae815ff72a361b0",
695 "upstreamSha256": "ed157ab34cb8dfaa25870ec24c116967a7d77e085ab4e5800ae815ff72a361b0"
696 },
697 {
698 "path": "react/hooks/useLocalParticipant.ts",
699 "package": "react",
700 "sha256": "74225abfdf0db4e092a44c72861152c4431aa761fb7c6b35217d0d1ded94da1c",
701 "upstreamSha256": "74225abfdf0db4e092a44c72861152c4431aa761fb7c6b35217d0d1ded94da1c"
702 },
703 {
704 "path": "react/hooks/useLocalParticipantPermissions.ts",
705 "package": "react",
706 "sha256": "597ef376c536ff17cee41b4a10407bfdae3f880e721c917ef445ab410757b266",
707 "upstreamSha256": "597ef376c536ff17cee41b4a10407bfdae3f880e721c917ef445ab410757b266"
708 },
709 {
710 "path": "react/hooks/useMediaDevices.ts",
711 "package": "react",
712 "sha256": "5cd9079c102d75eaf8d77fc6151fcb565db5d9831abe5a7992c37d1e90ba0cb6",
713 "upstreamSha256": "5cd9079c102d75eaf8d77fc6151fcb565db5d9831abe5a7992c37d1e90ba0cb6"
714 },
715 {
716 "path": "react/hooks/useMediaDeviceSelect.ts",
717 "package": "react",
718 "sha256": "5d3c0dd6215de2b53197defb6c8e10130e13aa22477611b702aa274c5ba8ed57",
719 "upstreamSha256": "5d3c0dd6215de2b53197defb6c8e10130e13aa22477611b702aa274c5ba8ed57"
720 },
721 {
722 "path": "react/hooks/useMediaTrackBySourceOrName.ts",
723 "package": "react",
724 "sha256": "dba42075d2d3fe785fe190bb49c2359f5379d609d03e198ac7337acb34ae09b3",
725 "upstreamSha256": "be8f17daa60881a5bfac801c61a562d49875f3f1107aa986b899fb079805adbf"
726 },
727 {
728 "path": "react/hooks/usePagination.ts",
729 "package": "react",
730 "sha256": "0ce86759feda9f52aad9d17a8ea2990b72a242ca6d4aead91a5ca5f7b464ccae",
731 "upstreamSha256": "0ce86759feda9f52aad9d17a8ea2990b72a242ca6d4aead91a5ca5f7b464ccae"
732 },
733 {
734 "path": "react/hooks/useParticipantAttributes.ts",
735 "package": "react",
736 "sha256": "f64914fd699ec1685b647072a9835fd94c6fec88d69fc20472481c2052217e51",
737 "upstreamSha256": "86e45c01e1a42b7ab2620ec013555c1044929e09f6925cb343fa0940292936d9"
738 },
739 {
740 "path": "react/hooks/useParticipantInfo.ts",
741 "package": "react",
742 "sha256": "92d97513ba360bf9c03731a6976703fecc66648fbb082f8ffbd4e5f8e16beef7",
743 "upstreamSha256": "92d97513ba360bf9c03731a6976703fecc66648fbb082f8ffbd4e5f8e16beef7"
744 },
745 {
746 "path": "react/hooks/useParticipantPermissions.ts",
747 "package": "react",
748 "sha256": "851c26bf3c4884a57fd76f9b7b5780ec694713bb31bc49af929dd4319402f894",
749 "upstreamSha256": "851c26bf3c4884a57fd76f9b7b5780ec694713bb31bc49af929dd4319402f894"
750 },
751 {
752 "path": "react/hooks/useParticipants.ts",
753 "package": "react",
754 "sha256": "dfac27889529af2284029be29c35efa569553ed3502675b2df9a4db58ec11cc6",
755 "upstreamSha256": "dfac27889529af2284029be29c35efa569553ed3502675b2df9a4db58ec11cc6"
756 },
757 {
758 "path": "react/hooks/useParticipantTile.ts",
759 "package": "react",
760 "sha256": "f54ede76afbc1d66d8034172b59b74885efa878aa0ab1e76253bf1347e25dbf4",
761 "upstreamSha256": "f54ede76afbc1d66d8034172b59b74885efa878aa0ab1e76253bf1347e25dbf4"
762 },
763 {
764 "path": "react/hooks/useParticipantTracks.ts",
765 "package": "react",
766 "sha256": "e472baf277fd2c1f87c3b808ba69b163d9026e93f691eb7469c95f519d9b8aa2",
767 "upstreamSha256": "e472baf277fd2c1f87c3b808ba69b163d9026e93f691eb7469c95f519d9b8aa2"
768 },
769 {
770 "path": "react/hooks/usePersistentUserChoices.ts",
771 "package": "react",
772 "sha256": "0569507430e555efd0e4b7028ed87cc3777b5d7c48f8d603793227caa8e9b592",
773 "upstreamSha256": "0569507430e555efd0e4b7028ed87cc3777b5d7c48f8d603793227caa8e9b592"
774 },
775 {
776 "path": "react/hooks/usePinnedTracks.ts",
777 "package": "react",
778 "sha256": "2cdd16d17e42af7d718af332c316674cacef2568293dd4e80e36f9b8e6fc0638",
779 "upstreamSha256": "2cdd16d17e42af7d718af332c316674cacef2568293dd4e80e36f9b8e6fc0638"
780 },
781 {
782 "path": "react/hooks/useRemoteParticipant.ts",
783 "package": "react",
784 "sha256": "f95dab7f055a9c4699f4d9c52c610f9a7fd21cf4204ebf92a7eb249930816420",
785 "upstreamSha256": "f95dab7f055a9c4699f4d9c52c610f9a7fd21cf4204ebf92a7eb249930816420"
786 },
787 {
788 "path": "react/hooks/useRemoteParticipants.ts",
789 "package": "react",
790 "sha256": "2b9abea6779097cf12c4266ce09d001e3fd3cc55f3fa8e16351c1fa6e2e228e1",
791 "upstreamSha256": "2b9abea6779097cf12c4266ce09d001e3fd3cc55f3fa8e16351c1fa6e2e228e1"
792 },
793 {
794 "path": "react/hooks/useRoomInfo.ts",
795 "package": "react",
796 "sha256": "060f01f9151f8f3c25269df32c21c693a33bd2159e94eb108c92d576f7dec706",
797 "upstreamSha256": "060f01f9151f8f3c25269df32c21c693a33bd2159e94eb108c92d576f7dec706"
798 },
799 {
800 "path": "react/hooks/useRpc.ts",
801 "package": "react",
802 "sha256": "bf79c7763b41e1b9f4c7a99bec7ac8411806e95326932e2c2a364183811ca657",
803 "upstreamSha256": "bf79c7763b41e1b9f4c7a99bec7ac8411806e95326932e2c2a364183811ca657"
804 },
805 {
806 "path": "react/hooks/useSequentialRoomConnectDisconnect.ts",
807 "package": "react",
808 "sha256": "55c03635b237507bbe9fd2647905f25815792b158ea3d3db7475bb29d31a6c53",
809 "upstreamSha256": "55c03635b237507bbe9fd2647905f25815792b158ea3d3db7475bb29d31a6c53"
810 },
811 {
812 "path": "react/hooks/useSession.ts",
813 "package": "react",
814 "sha256": "b677c02ede184826201c7e08046ffdb3e0e37fdcf670748c4db564df673bb922",
815 "upstreamSha256": "a692925f2461ef69d1632e4d7b68404cdbc2293a562bceb2ef187fdcd7840e0c"
816 },
817 {
818 "path": "react/hooks/useSessionMessages.ts",
819 "package": "react",
820 "sha256": "298551c38a24829ccf747172d6a4f75bf12a418b532312764e4b08ca24008557",
821 "upstreamSha256": "f0023b878fe38a7db9c50b811ab1881983b7682b01743f5204060b636f50fe7b"
822 },
823 {
824 "path": "react/hooks/useSettingsToggle.ts",
825 "package": "react",
826 "sha256": "6076fb5b92a17acdf16577547e10c7d46238e3573a9c752a8bfd13d9f373d019",
827 "upstreamSha256": "6076fb5b92a17acdf16577547e10c7d46238e3573a9c752a8bfd13d9f373d019"
828 },
829 {
830 "path": "react/hooks/useSortedParticipants.ts",
831 "package": "react",
832 "sha256": "6fad566ab9cf41b55629cfaa8581cdf2c65af200069993e04c2d9c6bf8fa078e",
833 "upstreamSha256": "6fad566ab9cf41b55629cfaa8581cdf2c65af200069993e04c2d9c6bf8fa078e"
834 },
835 {
836 "path": "react/hooks/useSpeakingParticipants.ts",
837 "package": "react",
838 "sha256": "0666364d3d12ed5631f5f6a9fc40f56a648c37d53bb42206407d3a71c579dc49",
839 "upstreamSha256": "0666364d3d12ed5631f5f6a9fc40f56a648c37d53bb42206407d3a71c579dc49"
840 },
841 {
842 "path": "react/hooks/useStartAudio.ts",
843 "package": "react",
844 "sha256": "405b1cc72d8fd0d8b9b4009a64ff93100a223788fabdf258c983c5d12c52c2c7",
845 "upstreamSha256": "405b1cc72d8fd0d8b9b4009a64ff93100a223788fabdf258c983c5d12c52c2c7"
846 },
847 {
848 "path": "react/hooks/useStartVideo.ts",
849 "package": "react",
850 "sha256": "b60f0c43a1be3168852c893b4c1ab10863dbf2cf152dbc3d91ddc3c1f85b1086",
851 "upstreamSha256": "b60f0c43a1be3168852c893b4c1ab10863dbf2cf152dbc3d91ddc3c1f85b1086"
852 },
853 {
854 "path": "react/hooks/useSwipe.ts",
855 "package": "react",
856 "sha256": "dcc3589a4e3f6e702261d71406fc183fb996cc63701f5c3664e902adc0cc0596",
857 "upstreamSha256": "bcf480340bbfaa7d79a4fb4b1924c25bb63c21b2de3ccf6d2ea4579f5cd6aeb7"
858 },
859 {
860 "path": "react/hooks/useTextStream.ts",
861 "package": "react",
862 "sha256": "e9cb326f2b1ab52b0422914adf618d2095409cb3636b3bf49f6a3872e87b602c",
863 "upstreamSha256": "e9cb326f2b1ab52b0422914adf618d2095409cb3636b3bf49f6a3872e87b602c"
864 },
865 {
866 "path": "react/hooks/useToken.ts",
867 "package": "react",
868 "sha256": "2cf8ec4cea58c46ed81c9fd0474970e11a342fbbc4799bff3436a0fd954165a8",
869 "upstreamSha256": "2cf8ec4cea58c46ed81c9fd0474970e11a342fbbc4799bff3436a0fd954165a8"
870 },
871 {
872 "path": "react/hooks/useTrack.ts",
873 "package": "react",
874 "sha256": "8d3185eecf923091350c4d3d9a72152c77f03f6f697630dd3c6b29b8a710ab8e",
875 "upstreamSha256": "8d3185eecf923091350c4d3d9a72152c77f03f6f697630dd3c6b29b8a710ab8e"
876 },
877 {
878 "path": "react/hooks/useTrackByName.ts",
879 "package": "react",
880 "sha256": "87f038d8f2123d7e41005d959a62c0ad0611fedd211d6f17aeaef6ebf2d40c03",
881 "upstreamSha256": "87f038d8f2123d7e41005d959a62c0ad0611fedd211d6f17aeaef6ebf2d40c03"
882 },
883 {
884 "path": "react/hooks/useTrackMutedIndicator.ts",
885 "package": "react",
886 "sha256": "cca68016e34a10c4a57c27aaf03b69bf56634535a7dfc5a352d84ae213766ec0",
887 "upstreamSha256": "cca68016e34a10c4a57c27aaf03b69bf56634535a7dfc5a352d84ae213766ec0"
888 },
889 {
890 "path": "react/hooks/useTrackRefBySourceOrName.ts",
891 "package": "react",
892 "sha256": "6e09d15c4cb5e2102c11fbc725433d345cae6e402b2ae78dc0bdd5ec765e4364",
893 "upstreamSha256": "6e09d15c4cb5e2102c11fbc725433d345cae6e402b2ae78dc0bdd5ec765e4364"
894 },
895 {
896 "path": "react/hooks/useTracks.ts",
897 "package": "react",
898 "sha256": "a8f963faa3edf98bec886569fcfe43dbd91786409cb89688de0e4cd925f151bd",
899 "upstreamSha256": "a8f963faa3edf98bec886569fcfe43dbd91786409cb89688de0e4cd925f151bd"
900 },
901 {
902 "path": "react/hooks/useTrackSyncTime.ts",
903 "package": "react",
904 "sha256": "e41a5052dd1dc015c03420c20a26c4e4e6c2ee052f4faf29ebe0f5657d7c0f4c",
905 "upstreamSha256": "e41a5052dd1dc015c03420c20a26c4e4e6c2ee052f4faf29ebe0f5657d7c0f4c"
906 },
907 {
908 "path": "react/hooks/useTrackToggle.ts",
909 "package": "react",
910 "sha256": "bce12b496d3be738ee71f42590947270abf850748d71841afd8992217ebe2a08",
911 "upstreamSha256": "bce12b496d3be738ee71f42590947270abf850748d71841afd8992217ebe2a08"
912 },
913 {
914 "path": "react/hooks/useTrackTranscription.ts",
915 "package": "react",
916 "sha256": "26e5f8870c9c242b075ea75281af568c2d53a09db1596de2a5a030bfeb610589",
917 "upstreamSha256": "26e5f8870c9c242b075ea75281af568c2d53a09db1596de2a5a030bfeb610589"
918 },
919 {
920 "path": "react/hooks/useTrackVolume.ts",
921 "package": "react",
922 "sha256": "53d26b85a6538108949c2419e4ebab7f7d4e0bec16b3fa1b0198680563f29d24",
923 "upstreamSha256": "4b895ff01fa44928bddb583e16552d72065603afe5e52c57590d59cf0c096507"
924 },
925 {
926 "path": "react/hooks/useTranscriptions.ts",
927 "package": "react",
928 "sha256": "faba43b94bec4b42af16708e2d9ce5fc5f7ba9a4564f18befbb1a2c703cb90fa",
929 "upstreamSha256": "faba43b94bec4b42af16708e2d9ce5fc5f7ba9a4564f18befbb1a2c703cb90fa"
930 },
931 {
932 "path": "react/hooks/useVisualStableUpdate.ts",
933 "package": "react",
934 "sha256": "dc73bc300f4a9a8bf1fefa5070e0cf1539bac6755208cb80f40018bc06b31625",
935 "upstreamSha256": "dc73bc300f4a9a8bf1fefa5070e0cf1539bac6755208cb80f40018bc06b31625"
936 },
937 {
938 "path": "react/hooks/useVoiceAssistant.ts",
939 "package": "react",
940 "sha256": "c604aafa7158f576fac27dbc9cf4d02207ca00ad33ca3f4473d1cca38b9d39e1",
941 "upstreamSha256": "8820db1896f31857e77cda7416ef1893a250be448dbbd2c632e1a98ad7dbe966"
942 },
943 {
944 "path": "react/hooks/useWarnAboutMissingStyles.ts",
945 "package": "react",
946 "sha256": "c366cf879252455c171ef9e19a2abfb5189178047a1c95f020c2b080c085bdfc",
947 "upstreamSha256": "c366cf879252455c171ef9e19a2abfb5189178047a1c95f020c2b080c085bdfc"
948 },
949 {
950 "path": "react/index.ts",
951 "package": "react",
952 "sha256": "3ca8ccf37a00af7427add635a534a2f46557c16a911f43c2a1e453f12ff3bebf",
953 "upstreamSha256": "3ca8ccf37a00af7427add635a534a2f46557c16a911f43c2a1e453f12ff3bebf"
954 },
955 {
956 "path": "react/mergeProps.ts",
957 "package": "react",
958 "sha256": "dc4bb469ea53147c495eb91853e779e3106f69be0aeabeb0f416ff4c98b58296",
959 "upstreamSha256": "dc4bb469ea53147c495eb91853e779e3106f69be0aeabeb0f416ff4c98b58296"
960 },
961 {
962 "path": "react/prefabs/AudioConference.tsx",
963 "package": "react",
964 "sha256": "6d55eb3ef6a05bec97a957ee25e05edacabb52509f9139e5fcf30f59dac04fb0",
965 "upstreamSha256": "6d55eb3ef6a05bec97a957ee25e05edacabb52509f9139e5fcf30f59dac04fb0"
966 },
967 {
968 "path": "react/prefabs/Chat.tsx",
969 "package": "react",
970 "sha256": "48618ad7d6985f33e308b8de68f10056b3c05bc4bd702ee94cc1d084fdb8e614",
971 "upstreamSha256": "a4f3a153f81568901d20451d2d18c145c128c81bebaa218fc1876ef43b69c01f"
972 },
973 {
974 "path": "react/prefabs/ControlBar.tsx",
975 "package": "react",
976 "sha256": "22ba4b456c04dbdc2d55168ceadfb346d300187d32b26d7783288ca1ee98d355",
977 "upstreamSha256": "3c1163f531cdf8c87f2566dca2ffc8d03bc7f1bac67b59bd81ba7854df0ecbff"
978 },
979 {
980 "path": "react/prefabs/index.ts",
981 "package": "react",
982 "sha256": "dc15ee54bfbd1cd20a7957c0e1e7f7b7cbf7dd178456219e902c679e8e46567f",
983 "upstreamSha256": "dc15ee54bfbd1cd20a7957c0e1e7f7b7cbf7dd178456219e902c679e8e46567f"
984 },
985 {
986 "path": "react/prefabs/MediaDeviceMenu.tsx",
987 "package": "react",
988 "sha256": "c6b5efc4f16121ad7a75c4a9c87c9a267dab1f4d3100cb3de0afd84e33842ecc",
989 "upstreamSha256": "3fad66b350d6f0515da3afb3dcda153b7f858d16fa00472f477593ecac6ea72f"
990 },
991 {
992 "path": "react/prefabs/PreJoin.tsx",
993 "package": "react",
994 "sha256": "d49543030486a92e5ae99de4631d258f388839be4dd58665b596cbee6d99446d",
995 "upstreamSha256": "d49543030486a92e5ae99de4631d258f388839be4dd58665b596cbee6d99446d"
996 },
997 {
998 "path": "react/prefabs/VideoConference.tsx",
999 "package": "react",
1000 "sha256": "90379f249b38d72c78989958761450b30409fbc817fea02e5b35f61dbdaa52ed",
1001 "upstreamSha256": "90379f249b38d72c78989958761450b30409fbc817fea02e5b35f61dbdaa52ed"
1002 },
1003 {
1004 "path": "react/prefabs/VoiceAssistantControlBar.tsx",
1005 "package": "react",
1006 "sha256": "241cede1132b4b722d1f425f2fdf7e533209f31386bc350a3361d3accd194bb9",
1007 "upstreamSha256": "241cede1132b4b722d1f425f2fdf7e533209f31386bc350a3361d3accd194bb9"
1008 },
1009 {
1010 "path": "react/types/events.d.ts",
1011 "package": "react",
1012 "sha256": "23806f26a38d58baa243b3b9a3178873dbf069e19c57b11eabc1df6ff4db7496",
1013 "upstreamSha256": null
1014 },
1015 {
1016 "path": "react/utils.ts",
1017 "package": "react",
1018 "sha256": "f072be3cc8ccb45cf7ce6623a0df43c63d3e9fa07e221e4a40338c09774cfefc",
1019 "upstreamSha256": "1ad1dd12dcfcbdf0a05b5f128d05bd6589f8bdad02ffbcd92bfd9ce4505460cf"
1020 },
1021 {
1022 "path": "core/components/chat.ts",
1023 "package": "core",
1024 "sha256": "d6dcb62b554f309386aac8d575f8afb9bf9941a6d5742fa177a0b3d4199291bb",
1025 "upstreamSha256": "b8c00cfd4a8d6dd6d9edbf6e348bfdd972f60cb79c7d3bad31a00af31d5477a9"
1026 },
1027 {
1028 "path": "core/components/chatToggle.ts",
1029 "package": "core",
1030 "sha256": "26feedfa4564a974c4ef4866652377b84cec20c2b138fc6214f9919b43f6390b",
1031 "upstreamSha256": "26feedfa4564a974c4ef4866652377b84cec20c2b138fc6214f9919b43f6390b"
1032 },
1033 {
1034 "path": "core/components/clearPinButton.ts",
1035 "package": "core",
1036 "sha256": "b671d10726b43163b0ed2c0511d3d62dab34e61ee41679fb5e3b53e9048f4de3",
1037 "upstreamSha256": "b671d10726b43163b0ed2c0511d3d62dab34e61ee41679fb5e3b53e9048f4de3"
1038 },
1039 {
1040 "path": "core/components/connectionQualityIndicator.ts",
1041 "package": "core",
1042 "sha256": "efc1e4c9ab1fc606d04b85398d6eaf956eb2e406d26f0b8b706878970dad3fa3",
1043 "upstreamSha256": "efc1e4c9ab1fc606d04b85398d6eaf956eb2e406d26f0b8b706878970dad3fa3"
1044 },
1045 {
1046 "path": "core/components/disconnectButton.ts",
1047 "package": "core",
1048 "sha256": "efd0c31cbdc9f8ec820289e151d9839ee225f79afc3c7e99ab95b3bdb982a3eb",
1049 "upstreamSha256": "efd0c31cbdc9f8ec820289e151d9839ee225f79afc3c7e99ab95b3bdb982a3eb"
1050 },
1051 {
1052 "path": "core/components/focusToggle.ts",
1053 "package": "core",
1054 "sha256": "572adb54b1f21e85cedfcc5752f50079338e4edf57ba52d9d520836499b528f5",
1055 "upstreamSha256": "572adb54b1f21e85cedfcc5752f50079338e4edf57ba52d9d520836499b528f5"
1056 },
1057 {
1058 "path": "core/components/mediaDeviceSelect.ts",
1059 "package": "core",
1060 "sha256": "a634affdfd5fe2aaaae5f6bf301224ee2bb974e3abdc32d5fcd84c9109c3af7a",
1061 "upstreamSha256": "a634affdfd5fe2aaaae5f6bf301224ee2bb974e3abdc32d5fcd84c9109c3af7a"
1062 },
1063 {
1064 "path": "core/components/mediaToggle.ts",
1065 "package": "core",
1066 "sha256": "21067fca1e8f9f46df52639d0815f395f173a0bb1ba477be0e206f9feabe701b",
1067 "upstreamSha256": "21067fca1e8f9f46df52639d0815f395f173a0bb1ba477be0e206f9feabe701b"
1068 },
1069 {
1070 "path": "core/components/mediaTrack.ts",
1071 "package": "core",
1072 "sha256": "f6eed471013171b4e1e349e4236fcedab47566813f19b048cf7c90879c2415de",
1073 "upstreamSha256": "f6eed471013171b4e1e349e4236fcedab47566813f19b048cf7c90879c2415de"
1074 },
1075 {
1076 "path": "core/components/participantName.ts",
1077 "package": "core",
1078 "sha256": "e444a0e1d18a64361715963ffa3811f5ef6fb0c43981b90b9180e69d9c6877d8",
1079 "upstreamSha256": "e444a0e1d18a64361715963ffa3811f5ef6fb0c43981b90b9180e69d9c6877d8"
1080 },
1081 {
1082 "path": "core/components/participantTile.ts",
1083 "package": "core",
1084 "sha256": "8699eb1a46496a226239ff25431b475426a5e67027da33f2de456214ae2b4c6e",
1085 "upstreamSha256": "8699eb1a46496a226239ff25431b475426a5e67027da33f2de456214ae2b4c6e"
1086 },
1087 {
1088 "path": "core/components/room.ts",
1089 "package": "core",
1090 "sha256": "aff71b54472a7756b7a7fd5232e1f155acf01343dd133999f63fbdec02ccdfd3",
1091 "upstreamSha256": "aff71b54472a7756b7a7fd5232e1f155acf01343dd133999f63fbdec02ccdfd3"
1092 },
1093 {
1094 "path": "core/components/startAudio.ts",
1095 "package": "core",
1096 "sha256": "b6ac07bab1d2e6cf8d920e384d604bada79bf0bb5c67d3528661d31ceef66139",
1097 "upstreamSha256": "b6ac07bab1d2e6cf8d920e384d604bada79bf0bb5c67d3528661d31ceef66139"
1098 },
1099 {
1100 "path": "core/components/startVideo.ts",
1101 "package": "core",
1102 "sha256": "919ffdaf8523eac29eeaf85a94c11bfd8c7804e8cf9d26fcce310624bd7a6e3d",
1103 "upstreamSha256": "919ffdaf8523eac29eeaf85a94c11bfd8c7804e8cf9d26fcce310624bd7a6e3d"
1104 },
1105 {
1106 "path": "core/components/textStream.ts",
1107 "package": "core",
1108 "sha256": "ab3763aa3f6ac7d7d04f5510bb0229cc0998d86bc8f2df5fcb8545687301b70e",
1109 "upstreamSha256": "ab3763aa3f6ac7d7d04f5510bb0229cc0998d86bc8f2df5fcb8545687301b70e"
1110 },
1111 {
1112 "path": "core/components/trackMutedIndicator.ts",
1113 "package": "core",
1114 "sha256": "7f3b0d24d5f4994d0f4178316b2152d0d81c27bc655bd53bd354827f8f97904b",
1115 "upstreamSha256": "7f3b0d24d5f4994d0f4178316b2152d0d81c27bc655bd53bd354827f8f97904b"
1116 },
1117 {
1118 "path": "core/constants.ts",
1119 "package": "core",
1120 "sha256": "862982afb2e3d7ed1fd1d9632c5a169c877b159d5ab562107302e660bc42392d",
1121 "upstreamSha256": "862982afb2e3d7ed1fd1d9632c5a169c877b159d5ab562107302e660bc42392d"
1122 },
1123 {
1124 "path": "core/helper/array-helper.ts",
1125 "package": "core",
1126 "sha256": "92ef8c75d3c575cbe5b44bce87b290ea1cb857eb3ce3f3aed8f9834ed7c08212",
1127 "upstreamSha256": "92ef8c75d3c575cbe5b44bce87b290ea1cb857eb3ce3f3aed8f9834ed7c08212"
1128 },
1129 {
1130 "path": "core/helper/detectMobileBrowser.ts",
1131 "package": "core",
1132 "sha256": "e7142b11db3950fefdd11e62d693348e2ba5d957677e95f53442eb4530c2311d",
1133 "upstreamSha256": "e7142b11db3950fefdd11e62d693348e2ba5d957677e95f53442eb4530c2311d"
1134 },
1135 {
1136 "path": "core/helper/emailRegex.ts",
1137 "package": "core",
1138 "sha256": "b310862402dc95878f4f81802291b90f043f2b178d306b35c15b07c2f7afa7c6",
1139 "upstreamSha256": "b310862402dc95878f4f81802291b90f043f2b178d306b35c15b07c2f7afa7c6"
1140 },
1141 {
1142 "path": "core/helper/eventGroups.ts",
1143 "package": "core",
1144 "sha256": "eb0c22a85e370e97b0505ee855ed0781e5eb5bcadd215ced293db796cf187d66",
1145 "upstreamSha256": "eb0c22a85e370e97b0505ee855ed0781e5eb5bcadd215ced293db796cf187d66"
1146 },
1147 {
1148 "path": "core/helper/featureDetection.ts",
1149 "package": "core",
1150 "sha256": "1a1f34c5b306174d006081285d3b0b142b2fb3af895a5e2cdfc6508e21208bc9",
1151 "upstreamSha256": "1a1f34c5b306174d006081285d3b0b142b2fb3af895a5e2cdfc6508e21208bc9"
1152 },
1153 {
1154 "path": "core/helper/floating-menu.ts",
1155 "package": "core",
1156 "sha256": "153d0cd9b31088783b7093aa99b817401157f602bac37b8096d512fc95028cfd",
1157 "upstreamSha256": "153d0cd9b31088783b7093aa99b817401157f602bac37b8096d512fc95028cfd"
1158 },
1159 {
1160 "path": "core/helper/future.ts",
1161 "package": "core",
1162 "sha256": "4fad622da9230f47d8f22d06d51e00db832bade99eedc5abdd653969e96aa6e3",
1163 "upstreamSha256": "4fad622da9230f47d8f22d06d51e00db832bade99eedc5abdd653969e96aa6e3"
1164 },
1165 {
1166 "path": "core/helper/grid-layouts.ts",
1167 "package": "core",
1168 "sha256": "63ef9fb8d61402e1546d5a3baad4c18f8241f6404773ba28fbc39ba15e7b320f",
1169 "upstreamSha256": "63ef9fb8d61402e1546d5a3baad4c18f8241f6404773ba28fbc39ba15e7b320f"
1170 },
1171 {
1172 "path": "core/helper/index.ts",
1173 "package": "core",
1174 "sha256": "72d225d42290b6eb3d640a5cc242a3786bd3b4952dc51ad6d794a93554f25cad",
1175 "upstreamSha256": "72d225d42290b6eb3d640a5cc242a3786bd3b4952dc51ad6d794a93554f25cad"
1176 },
1177 {
1178 "path": "core/helper/participant-attributes.ts",
1179 "package": "core",
1180 "sha256": "b2c3efde34ec721672fb376a29ed0d2e5657364ed2c80eb4ee68fbc1a0e4a2d3",
1181 "upstreamSha256": "75c7787dff8ab78fc76bc5ab65c4c2db7a9cdb9bf4ad074f734f1a06f1eb5acc"
1182 },
1183 {
1184 "path": "core/helper/set-helper.ts",
1185 "package": "core",
1186 "sha256": "3cbc7ab5867e310c8206aceb3c24bc55c6ffb7eeb20be630273f735c509d33e6",
1187 "upstreamSha256": "3cbc7ab5867e310c8206aceb3c24bc55c6ffb7eeb20be630273f735c509d33e6"
1188 },
1189 {
1190 "path": "core/helper/tokenizer.ts",
1191 "package": "core",
1192 "sha256": "cafa4acd5a8d7113ac8d21c298d04782f41a0aeb442874069530f782b9f1004c",
1193 "upstreamSha256": "cafa4acd5a8d7113ac8d21c298d04782f41a0aeb442874069530f782b9f1004c"
1194 },
1195 {
1196 "path": "core/helper/transcriptions.ts",
1197 "package": "core",
1198 "sha256": "681981e2b0e81e8cdd0952468328711bc17d6a46a4129f7df373d9a340e02ce4",
1199 "upstreamSha256": "681981e2b0e81e8cdd0952468328711bc17d6a46a4129f7df373d9a340e02ce4"
1200 },
1201 {
1202 "path": "core/helper/url-regex.ts",
1203 "package": "core",
1204 "sha256": "a11b497cb24dbbe5b852abdfe4556fa8eb7a114554b399224e5cb2f7498bb980",
1205 "upstreamSha256": "a11b497cb24dbbe5b852abdfe4556fa8eb7a114554b399224e5cb2f7498bb980"
1206 },
1207 {
1208 "path": "core/index.ts",
1209 "package": "core",
1210 "sha256": "68cbf4985194a5138d5d30c374620dc81e2071043ca4ad104704cda0bf521695",
1211 "upstreamSha256": "68cbf4985194a5138d5d30c374620dc81e2071043ca4ad104704cda0bf521695"
1212 },
1213 {
1214 "path": "core/logger.ts",
1215 "package": "core",
1216 "sha256": "f6d8e7b62d6546a4959d9ff58ba569d43ac007d39adf2900134b05e008eba568",
1217 "upstreamSha256": "f6d8e7b62d6546a4959d9ff58ba569d43ac007d39adf2900134b05e008eba568"
1218 },
1219 {
1220 "path": "core/messages/index.ts",
1221 "package": "core",
1222 "sha256": "d5c19655468e29f60c871b21e73af8ebc653f736e7123ade916f22c4a5f80ce5",
1223 "upstreamSha256": "d5c19655468e29f60c871b21e73af8ebc653f736e7123ade916f22c4a5f80ce5"
1224 },
1225 {
1226 "path": "core/messages/types.ts",
1227 "package": "core",
1228 "sha256": "8441bd64fba8ccb9d24307d0866c2513f312541ad0a5e65c495d4bd1af43bad0",
1229 "upstreamSha256": "8441bd64fba8ccb9d24307d0866c2513f312541ad0a5e65c495d4bd1af43bad0"
1230 },
1231 {
1232 "path": "core/observables/dataChannel.ts",
1233 "package": "core",
1234 "sha256": "6be68c5222405ca3c7e85dc27ba4924d47e97fd2efe38a7ab7aec682df9405c9",
1235 "upstreamSha256": "a9a3d4095ed34d10363adba77dc6887527301948c95638b5b9115a86db253b55"
1236 },
1237 {
1238 "path": "core/observables/dom-event.ts",
1239 "package": "core",
1240 "sha256": "9d449feef519d78ef9ef2541853ba9d275283577cc7c360a5a304fb150a89ffa",
1241 "upstreamSha256": "9d449feef519d78ef9ef2541853ba9d275283577cc7c360a5a304fb150a89ffa"
1242 },
1243 {
1244 "path": "core/observables/participant.ts",
1245 "package": "core",
1246 "sha256": "b0d87029b89de3eefff2ddf3258a1d2e415259981103cd42813c354459aa0243",
1247 "upstreamSha256": "b0d87029b89de3eefff2ddf3258a1d2e415259981103cd42813c354459aa0243"
1248 },
1249 {
1250 "path": "core/observables/room.ts",
1251 "package": "core",
1252 "sha256": "41eba945704f654fbb1c5ea38f17c0e8a632ae2c6c061467237417d8e7560633",
1253 "upstreamSha256": "41eba945704f654fbb1c5ea38f17c0e8a632ae2c6c061467237417d8e7560633"
1254 },
1255 {
1256 "path": "core/observables/track.ts",
1257 "package": "core",
1258 "sha256": "a76cbf37579594df4a6dc5429e86a704421c7d9d9ae12e58b8a4d1bede2e7694",
1259 "upstreamSha256": "edade116d2ed2e64b41199352f601e4fdf897f502c8daf5973684bae8f473001"
1260 },
1261 {
1262 "path": "core/persistent-storage/index.ts",
1263 "package": "core",
1264 "sha256": "9c8cc8e7e95d684ba063867d658a785a205e6d428006fc31ccdef7855a0be883",
1265 "upstreamSha256": "9c8cc8e7e95d684ba063867d658a785a205e6d428006fc31ccdef7855a0be883"
1266 },
1267 {
1268 "path": "core/persistent-storage/local-storage-helpers.ts",
1269 "package": "core",
1270 "sha256": "b8d1d84f215099076e189c43d5c34b1e169cf69193d57f3fd9cad1781ebf46a6",
1271 "upstreamSha256": "b8d1d84f215099076e189c43d5c34b1e169cf69193d57f3fd9cad1781ebf46a6"
1272 },
1273 {
1274 "path": "core/persistent-storage/user-choices.ts",
1275 "package": "core",
1276 "sha256": "63411b1d8977d1f9cece56a2271e0893ed8aeaf9362012d65efe58aa1ccae50a",
1277 "upstreamSha256": "63411b1d8977d1f9cece56a2271e0893ed8aeaf9362012d65efe58aa1ccae50a"
1278 },
1279 {
1280 "path": "core/sorting/base-sort-functions.ts",
1281 "package": "core",
1282 "sha256": "89910779bfd000f2111d906e37edf4af30a95549e45a0e4bdf47b5af86d290e9",
1283 "upstreamSha256": "89910779bfd000f2111d906e37edf4af30a95549e45a0e4bdf47b5af86d290e9"
1284 },
1285 {
1286 "path": "core/sorting/index.ts",
1287 "package": "core",
1288 "sha256": "c2ce950d7b5c80c42261bb7487415fa91db565eeceb9b397fa0b92cbf9db2635",
1289 "upstreamSha256": "c2ce950d7b5c80c42261bb7487415fa91db565eeceb9b397fa0b92cbf9db2635"
1290 },
1291 {
1292 "path": "core/sorting/sort-participants.ts",
1293 "package": "core",
1294 "sha256": "8b570135732d9a3ba97124724c1a9862825a2e18a666a053834039b42b81d08a",
1295 "upstreamSha256": "8b570135732d9a3ba97124724c1a9862825a2e18a666a053834039b42b81d08a"
1296 },
1297 {
1298 "path": "core/sorting/sort-track-bundles.ts",
1299 "package": "core",
1300 "sha256": "b5a7a4550dd4f6a272cd97bd1816f028341801181abf08ab605191d2f19aea57",
1301 "upstreamSha256": "b5a7a4550dd4f6a272cd97bd1816f028341801181abf08ab605191d2f19aea57"
1302 },
1303 {
1304 "path": "core/sorting/tile-array-update.ts",
1305 "package": "core",
1306 "sha256": "86e76c792edb0a03b136d7ac5aaef4769633bac882140c809dfb537d55d1d7b5",
1307 "upstreamSha256": "86e76c792edb0a03b136d7ac5aaef4769633bac882140c809dfb537d55d1d7b5"
1308 },
1309 {
1310 "path": "core/styles-interface/class-prefixer.ts",
1311 "package": "core",
1312 "sha256": "42e46a01e59f0134c1b5e30ae754bb24dd5156c5338ab5e5d8ab8ebd660a6ed3",
1313 "upstreamSha256": "42e46a01e59f0134c1b5e30ae754bb24dd5156c5338ab5e5d8ab8ebd660a6ed3"
1314 },
1315 {
1316 "path": "core/styles-interface/index.ts",
1317 "package": "core",
1318 "sha256": "ee2c996a5e7d0f1802cc23adf78cc5c99464cb02e11f834856d542e8b26b84ac",
1319 "upstreamSha256": "ee2c996a5e7d0f1802cc23adf78cc5c99464cb02e11f834856d542e8b26b84ac"
1320 },
1321 {
1322 "path": "core/track-reference/index.ts",
1323 "package": "core",
1324 "sha256": "cc19642bf5c06428dff00f154ced52577a6979717944550bedb5ac213e12100b",
1325 "upstreamSha256": "cc19642bf5c06428dff00f154ced52577a6979717944550bedb5ac213e12100b"
1326 },
1327 {
1328 "path": "core/track-reference/test-utils.ts",
1329 "package": "core",
1330 "sha256": "f84a50863a0108b515957162f6312a91e9227144aca0476f8face7112ac5e485",
1331 "upstreamSha256": "f84a50863a0108b515957162f6312a91e9227144aca0476f8face7112ac5e485"
1332 },
1333 {
1334 "path": "core/track-reference/track-reference.types.ts",
1335 "package": "core",
1336 "sha256": "bdc4a5b4309fbb3346d1b57837833c6c683f257a1916fb42aa45a99a5ebd8ef4",
1337 "upstreamSha256": "bdc4a5b4309fbb3346d1b57837833c6c683f257a1916fb42aa45a99a5ebd8ef4"
1338 },
1339 {
1340 "path": "core/track-reference/track-reference.utils.ts",
1341 "package": "core",
1342 "sha256": "f85e144be3adc86241bdf7febd27fd1ee13480b7325270b309fe457a13cfa1c9",
1343 "upstreamSha256": "f85e144be3adc86241bdf7febd27fd1ee13480b7325270b309fe457a13cfa1c9"
1344 },
1345 {
1346 "path": "core/types.ts",
1347 "package": "core",
1348 "sha256": "8f78902ff9b189bdc61e74bd69a7b23c4d475a466030fe436bb1c34e15aba0f0",
1349 "upstreamSha256": "8f78902ff9b189bdc61e74bd69a7b23c4d475a466030fe436bb1c34e15aba0f0"
1350 },
1351 {
1352 "path": "core/utils.ts",
1353 "package": "core",
1354 "sha256": "a8cfb4628068579687fde6784ef2ff9125c7b4707bc2f42b6be3d027fcd7c377",
1355 "upstreamSha256": "a8cfb4628068579687fde6784ef2ff9125c7b4707bc2f42b6be3d027fcd7c377"
1356 },
1357 {
1358 "path": "styles/dist/general/components/controls/index.css",
1359 "package": "styles",
1360 "sha256": "9ec4eff861c1fc1cabd34273dfa829d546e1a698391940c6541d388ef9cd0256",
1361 "upstreamSha256": "9ec4eff861c1fc1cabd34273dfa829d546e1a698391940c6541d388ef9cd0256"
1362 },
1363 {
1364 "path": "styles/dist/general/components/controls/index.css.map",
1365 "package": "styles",
1366 "sha256": "80b678ce631891212d6997aad4fc9b2a672c9c06a87b6650530c9afeef6b517e",
1367 "upstreamSha256": "80b678ce631891212d6997aad4fc9b2a672c9c06a87b6650530c9afeef6b517e"
1368 },
1369 {
1370 "path": "styles/dist/general/components/index.css",
1371 "package": "styles",
1372 "sha256": "7fa20007efa53072a9d8c1e2a083f5dc860b2f4c8dcfbdce643d4eee60125884",
1373 "upstreamSha256": "7fa20007efa53072a9d8c1e2a083f5dc860b2f4c8dcfbdce643d4eee60125884"
1374 },
1375 {
1376 "path": "styles/dist/general/components/index.css.map",
1377 "package": "styles",
1378 "sha256": "aa8ba4246e945d8ff7fde9fda5fb2f93141f360feef8a3bf75fc2147339f6d33",
1379 "upstreamSha256": "aa8ba4246e945d8ff7fde9fda5fb2f93141f360feef8a3bf75fc2147339f6d33"
1380 },
1381 {
1382 "path": "styles/dist/general/components/layout/index.css",
1383 "package": "styles",
1384 "sha256": "af7bd51c338eb20d2d4b59b71eaf71de89579cd4f4e887d3c15cd73af788e7ec",
1385 "upstreamSha256": "af7bd51c338eb20d2d4b59b71eaf71de89579cd4f4e887d3c15cd73af788e7ec"
1386 },
1387 {
1388 "path": "styles/dist/general/components/layout/index.css.map",
1389 "package": "styles",
1390 "sha256": "b094be7deaafe6d7fb4ce98802e7ece7240d54b2a0558942cd474dd86a0d48a8",
1391 "upstreamSha256": "b094be7deaafe6d7fb4ce98802e7ece7240d54b2a0558942cd474dd86a0d48a8"
1392 },
1393 {
1394 "path": "styles/dist/general/components/participant/index.css",
1395 "package": "styles",
1396 "sha256": "defd5cec4db51231bb7c81b1e8807e9c7eb1e367b157cf425456e8a5910ed257",
1397 "upstreamSha256": "defd5cec4db51231bb7c81b1e8807e9c7eb1e367b157cf425456e8a5910ed257"
1398 },
1399 {
1400 "path": "styles/dist/general/components/participant/index.css.map",
1401 "package": "styles",
1402 "sha256": "ea0439bac73919995f62ad415ce05fc99517f47eb3d4f18a6df4298fc22cd528",
1403 "upstreamSha256": "ea0439bac73919995f62ad415ce05fc99517f47eb3d4f18a6df4298fc22cd528"
1404 },
1405 {
1406 "path": "styles/dist/general/index.css",
1407 "package": "styles",
1408 "sha256": "9171d6d70be284f78d880a2f6143feea505394d17c721ea512a3893d659f71b9",
1409 "upstreamSha256": "9171d6d70be284f78d880a2f6143feea505394d17c721ea512a3893d659f71b9"
1410 },
1411 {
1412 "path": "styles/dist/general/index.css.map",
1413 "package": "styles",
1414 "sha256": "f6e2dd82e94ce2ec36ffdb6967dd92c21417b49f1eeb930a0213eaed9455db93",
1415 "upstreamSha256": "f6e2dd82e94ce2ec36ffdb6967dd92c21417b49f1eeb930a0213eaed9455db93"
1416 },
1417 {
1418 "path": "styles/dist/general/prefabs/audio-conference.css",
1419 "package": "styles",
1420 "sha256": "9b2997f45e7d346b4fbbf9ea08c07af2fc57c036e88b31660df54633b404c2d6",
1421 "upstreamSha256": "9b2997f45e7d346b4fbbf9ea08c07af2fc57c036e88b31660df54633b404c2d6"
1422 },
1423 {
1424 "path": "styles/dist/general/prefabs/audio-conference.css.map",
1425 "package": "styles",
1426 "sha256": "3b1d6b9c308714ff32b19b493c4033968ee7b7fe6c7572118ddc0b20e2aa966d",
1427 "upstreamSha256": "3b1d6b9c308714ff32b19b493c4033968ee7b7fe6c7572118ddc0b20e2aa966d"
1428 },
1429 {
1430 "path": "styles/dist/general/prefabs/chat.css",
1431 "package": "styles",
1432 "sha256": "a83f833bf4e528c33a5c08dc7f4dc182dc4cc8865c91418d009709c24512aa4a",
1433 "upstreamSha256": "a83f833bf4e528c33a5c08dc7f4dc182dc4cc8865c91418d009709c24512aa4a"
1434 },
1435 {
1436 "path": "styles/dist/general/prefabs/chat.css.map",
1437 "package": "styles",
1438 "sha256": "1584c2257adea7a8de3c220820a523fbd4bbbb6cc6fb23fc4cd06fbfdb910208",
1439 "upstreamSha256": "1584c2257adea7a8de3c220820a523fbd4bbbb6cc6fb23fc4cd06fbfdb910208"
1440 },
1441 {
1442 "path": "styles/dist/general/prefabs/control-bar.css",
1443 "package": "styles",
1444 "sha256": "ab21a0273c97bcaa88e90d36d388ba497951c2ddb785f29c5242336291b7f103",
1445 "upstreamSha256": "ab21a0273c97bcaa88e90d36d388ba497951c2ddb785f29c5242336291b7f103"
1446 },
1447 {
1448 "path": "styles/dist/general/prefabs/control-bar.css.map",
1449 "package": "styles",
1450 "sha256": "3101011e974081aae7e91b295f537c2cd4b1df7992fc30c5a5b5156a193942af",
1451 "upstreamSha256": "3101011e974081aae7e91b295f537c2cd4b1df7992fc30c5a5b5156a193942af"
1452 },
1453 {
1454 "path": "styles/dist/general/prefabs/index.css",
1455 "package": "styles",
1456 "sha256": "78ee7c6a6d9bb6ab8e0bc9fe8d8b522beef648016b5b219dd82325791f14d26b",
1457 "upstreamSha256": "78ee7c6a6d9bb6ab8e0bc9fe8d8b522beef648016b5b219dd82325791f14d26b"
1458 },
1459 {
1460 "path": "styles/dist/general/prefabs/index.css.map",
1461 "package": "styles",
1462 "sha256": "7b3da2f792929ec9835ca55d0c3560767db805ce199bf900dd21997da9558936",
1463 "upstreamSha256": "7b3da2f792929ec9835ca55d0c3560767db805ce199bf900dd21997da9558936"
1464 },
1465 {
1466 "path": "styles/dist/general/prefabs/prejoin.css",
1467 "package": "styles",
1468 "sha256": "f2e8a3298db2184cd54a6e8c2515691a8cb3dc6c21ce41aa1caf0fae94def731",
1469 "upstreamSha256": "f2e8a3298db2184cd54a6e8c2515691a8cb3dc6c21ce41aa1caf0fae94def731"
1470 },
1471 {
1472 "path": "styles/dist/general/prefabs/prejoin.css.map",
1473 "package": "styles",
1474 "sha256": "86634a1f44dde63a697b08ba484b3ce1daaa8cadb1d3a56740ed5982999592d1",
1475 "upstreamSha256": "86634a1f44dde63a697b08ba484b3ce1daaa8cadb1d3a56740ed5982999592d1"
1476 },
1477 {
1478 "path": "styles/dist/general/prefabs/video-conference.css",
1479 "package": "styles",
1480 "sha256": "6edc6fd56043c6a61ed5ada1585a4731901f93c59d476b2aa9c0bf67654aebe8",
1481 "upstreamSha256": "6edc6fd56043c6a61ed5ada1585a4731901f93c59d476b2aa9c0bf67654aebe8"
1482 },
1483 {
1484 "path": "styles/dist/general/prefabs/video-conference.css.map",
1485 "package": "styles",
1486 "sha256": "6341406a07635cbd1b829c86c9420b94307b8e009e8b69d4fbf3735986f00d8f",
1487 "upstreamSha256": "6341406a07635cbd1b829c86c9420b94307b8e009e8b69d4fbf3735986f00d8f"
1488 },
1489 {
1490 "path": "styles/dist/general/themes/default.css",
1491 "package": "styles",
1492 "sha256": "f24058bfb8131bd7dc7be5b1f1c1329c94c048e067af82b3a922acbe0eb4e033",
1493 "upstreamSha256": "f24058bfb8131bd7dc7be5b1f1c1329c94c048e067af82b3a922acbe0eb4e033"
1494 },
1495 {
1496 "path": "styles/dist/general/themes/default.css.map",
1497 "package": "styles",
1498 "sha256": "94431b22fdf377c06b1703977f8d868ce793d4a05ba5ea825247210f770b139e",
1499 "upstreamSha256": "94431b22fdf377c06b1703977f8d868ce793d4a05ba5ea825247210f770b139e"
1500 },
1501 {
1502 "path": "styles/dist/general/themes/huddle.css",
1503 "package": "styles",
1504 "sha256": "7acba9a0b95f800f4a4e0d8ac7e329fecee1c2879b0de759234a8a3129a382df",
1505 "upstreamSha256": "7acba9a0b95f800f4a4e0d8ac7e329fecee1c2879b0de759234a8a3129a382df"
1506 },
1507 {
1508 "path": "styles/dist/general/themes/huddle.css.map",
1509 "package": "styles",
1510 "sha256": "e0aaf31220017f2e74eceb7308391203302da6038b18bf1807e937012c564ac2",
1511 "upstreamSha256": "e0aaf31220017f2e74eceb7308391203302da6038b18bf1807e937012c564ac2"
1512 },
1513 {
1514 "path": "styles/dist/types_unprefixed/components/controls/index.scss.d.ts",
1515 "package": "styles",
1516 "sha256": "133050d171aff635fb6b6d19487839accb8d6189fc338b3e1ea0c467967b8d5a",
1517 "upstreamSha256": "133050d171aff635fb6b6d19487839accb8d6189fc338b3e1ea0c467967b8d5a"
1518 },
1519 {
1520 "path": "styles/dist/types_unprefixed/components/controls/_button.scss.d.ts",
1521 "package": "styles",
1522 "sha256": "ebd3a434fef83d57453f098d40f6926cff193aff8fe390fa57fa0fe7ed50bd8c",
1523 "upstreamSha256": "ebd3a434fef83d57453f098d40f6926cff193aff8fe390fa57fa0fe7ed50bd8c"
1524 },
1525 {
1526 "path": "styles/dist/types_unprefixed/components/controls/_chat-toggle.scss.d.ts",
1527 "package": "styles",
1528 "sha256": "7c7f27b89595210f3b9badce814ae728b891e20d6ca7e08fcfaad0bc4b2f58b3",
1529 "upstreamSha256": "7c7f27b89595210f3b9badce814ae728b891e20d6ca7e08fcfaad0bc4b2f58b3"
1530 },
1531 {
1532 "path": "styles/dist/types_unprefixed/components/controls/_disconnect-button.scss.d.ts",
1533 "package": "styles",
1534 "sha256": "86092ffabebfd5b8c96b9aa27527a695dd02cf472e4d1c9b07ba01a4f10e8b02",
1535 "upstreamSha256": "86092ffabebfd5b8c96b9aa27527a695dd02cf472e4d1c9b07ba01a4f10e8b02"
1536 },
1537 {
1538 "path": "styles/dist/types_unprefixed/components/controls/_media-device-select.scss.d.ts",
1539 "package": "styles",
1540 "sha256": "27d824f0b159f22ca56193931e8399353b92c89cc80c9710d960de1fa265d9b7",
1541 "upstreamSha256": "27d824f0b159f22ca56193931e8399353b92c89cc80c9710d960de1fa265d9b7"
1542 },
1543 {
1544 "path": "styles/dist/types_unprefixed/components/controls/_pagination-control.scss.d.ts",
1545 "package": "styles",
1546 "sha256": "a10b28ae6d0c3c5c6f29e5fc3c0ea00d426aeb6e918421ae9e0496be61ee2982",
1547 "upstreamSha256": "a10b28ae6d0c3c5c6f29e5fc3c0ea00d426aeb6e918421ae9e0496be61ee2982"
1548 },
1549 {
1550 "path": "styles/dist/types_unprefixed/components/controls/_pagination-indicator.scss.d.ts",
1551 "package": "styles",
1552 "sha256": "d5d01ccfe93e7b7ace7c06497689acf3b348836bf250e63b08ead9953dd901ca",
1553 "upstreamSha256": "d5d01ccfe93e7b7ace7c06497689acf3b348836bf250e63b08ead9953dd901ca"
1554 },
1555 {
1556 "path": "styles/dist/types_unprefixed/components/controls/_start-audio.scss.d.ts",
1557 "package": "styles",
1558 "sha256": "8ee292e818fb8212131aa19cdef2b7fb6f7bb68df0802621848b0f7f3dff1bb3",
1559 "upstreamSha256": "8ee292e818fb8212131aa19cdef2b7fb6f7bb68df0802621848b0f7f3dff1bb3"
1560 },
1561 {
1562 "path": "styles/dist/types_unprefixed/components/index.scss.d.ts",
1563 "package": "styles",
1564 "sha256": "c6e1743566bef0618be6d595d87c492491bbd2121f58f66495b76493d53222ef",
1565 "upstreamSha256": "c6e1743566bef0618be6d595d87c492491bbd2121f58f66495b76493d53222ef"
1566 },
1567 {
1568 "path": "styles/dist/types_unprefixed/components/layout/index.scss.d.ts",
1569 "package": "styles",
1570 "sha256": "0f35fe7ee88f2c67aea6766191fefa4c436fa8d4209330126557dbd888ccc36d",
1571 "upstreamSha256": "0f35fe7ee88f2c67aea6766191fefa4c436fa8d4209330126557dbd888ccc36d"
1572 },
1573 {
1574 "path": "styles/dist/types_unprefixed/components/layout/_carousel-view.scss.d.ts",
1575 "package": "styles",
1576 "sha256": "da7cf18bb713a53989efb3dc8bee5fbd1865de4f9bc01c8d714980045b6214e5",
1577 "upstreamSha256": "da7cf18bb713a53989efb3dc8bee5fbd1865de4f9bc01c8d714980045b6214e5"
1578 },
1579 {
1580 "path": "styles/dist/types_unprefixed/components/layout/_focus-layout.scss.d.ts",
1581 "package": "styles",
1582 "sha256": "06726c6e8586e1bcead103fed9457e4ff4549396a72c19a559d1d7b9a7844a75",
1583 "upstreamSha256": "06726c6e8586e1bcead103fed9457e4ff4549396a72c19a559d1d7b9a7844a75"
1584 },
1585 {
1586 "path": "styles/dist/types_unprefixed/components/layout/_grid-layout.scss.d.ts",
1587 "package": "styles",
1588 "sha256": "e10592085ab3e4b3fca898ce6b03b82c7257326a6354f2e1254efe72b126de7d",
1589 "upstreamSha256": "e10592085ab3e4b3fca898ce6b03b82c7257326a6354f2e1254efe72b126de7d"
1590 },
1591 {
1592 "path": "styles/dist/types_unprefixed/components/participant/index.scss.d.ts",
1593 "package": "styles",
1594 "sha256": "35aff480631cd08f8c5974f48fde8c68454a57baa0746b6262dec1d3e30cb91b",
1595 "upstreamSha256": "35aff480631cd08f8c5974f48fde8c68454a57baa0746b6262dec1d3e30cb91b"
1596 },
1597 {
1598 "path": "styles/dist/types_unprefixed/components/participant/_audio-visualizer.scss.d.ts",
1599 "package": "styles",
1600 "sha256": "a6467f0bd9d644198837738bd634a5efe43f581314716d76e241a3d9e198e3dd",
1601 "upstreamSha256": "a6467f0bd9d644198837738bd634a5efe43f581314716d76e241a3d9e198e3dd"
1602 },
1603 {
1604 "path": "styles/dist/types_unprefixed/components/participant/_connection-quality.scss.d.ts",
1605 "package": "styles",
1606 "sha256": "42cc6fe3e31f03a1808eb5c4d87e3884387c58b6027096fa3560ab88c5a5b59e",
1607 "upstreamSha256": "42cc6fe3e31f03a1808eb5c4d87e3884387c58b6027096fa3560ab88c5a5b59e"
1608 },
1609 {
1610 "path": "styles/dist/types_unprefixed/components/participant/_participant-media.scss.d.ts",
1611 "package": "styles",
1612 "sha256": "001612ba4e370eb5d23eacdb89dab8355596184f1fbdab4a78741d981acbd510",
1613 "upstreamSha256": "001612ba4e370eb5d23eacdb89dab8355596184f1fbdab4a78741d981acbd510"
1614 },
1615 {
1616 "path": "styles/dist/types_unprefixed/components/participant/_participant-name.scss.d.ts",
1617 "package": "styles",
1618 "sha256": "c32cc96dd64acf7ce95adba68d64306a3a0a2dc809fd50ca5abe30201ca3aa62",
1619 "upstreamSha256": "c32cc96dd64acf7ce95adba68d64306a3a0a2dc809fd50ca5abe30201ca3aa62"
1620 },
1621 {
1622 "path": "styles/dist/types_unprefixed/components/participant/_participant-tile.scss.d.ts",
1623 "package": "styles",
1624 "sha256": "00e025c91e8ec927fe8a20b4cf713b9ee0c37fb621cfe38fc02c8b4ab7699232",
1625 "upstreamSha256": "00e025c91e8ec927fe8a20b4cf713b9ee0c37fb621cfe38fc02c8b4ab7699232"
1626 },
1627 {
1628 "path": "styles/dist/types_unprefixed/components/participant/_track-muted-indicator.scss.d.ts",
1629 "package": "styles",
1630 "sha256": "2293f302e4d3eae3ce2a1b4b6ccfbc9153d8bdedd4310ecf2f6c8e3da064549e",
1631 "upstreamSha256": "2293f302e4d3eae3ce2a1b4b6ccfbc9153d8bdedd4310ecf2f6c8e3da064549e"
1632 },
1633 {
1634 "path": "styles/dist/types_unprefixed/components/_room.scss.d.ts",
1635 "package": "styles",
1636 "sha256": "f3f369777a8bc38eb1978977826673bf1a5aee9959a094507c8fddb727ca54b5",
1637 "upstreamSha256": "f3f369777a8bc38eb1978977826673bf1a5aee9959a094507c8fddb727ca54b5"
1638 },
1639 {
1640 "path": "styles/dist/types_unprefixed/components/_toast.scss.d.ts",
1641 "package": "styles",
1642 "sha256": "2be48ad0b5450dcb84dff5f7d4a8934ed5c7464236294c13bfc6c7d92b8f7643",
1643 "upstreamSha256": "2be48ad0b5450dcb84dff5f7d4a8934ed5c7464236294c13bfc6c7d92b8f7643"
1644 },
1645 {
1646 "path": "styles/dist/types_unprefixed/index.scss.d.ts",
1647 "package": "styles",
1648 "sha256": "2e455040dd69a4d167fcfb4062f51a3c1379e31569db688518ff4139e003c477",
1649 "upstreamSha256": "2e455040dd69a4d167fcfb4062f51a3c1379e31569db688518ff4139e003c477"
1650 },
1651 {
1652 "path": "styles/dist/types_unprefixed/prefabs/audio-conference.scss.d.ts",
1653 "package": "styles",
1654 "sha256": "17b934cf78827147899504134e1684a0553bce41651af036f2f96d5ca905b472",
1655 "upstreamSha256": "17b934cf78827147899504134e1684a0553bce41651af036f2f96d5ca905b472"
1656 },
1657 {
1658 "path": "styles/dist/types_unprefixed/prefabs/chat.scss.d.ts",
1659 "package": "styles",
1660 "sha256": "279077b7a00bc45272171c7091e7246de724b66cf46700a5d66ca1782d30e27a",
1661 "upstreamSha256": "279077b7a00bc45272171c7091e7246de724b66cf46700a5d66ca1782d30e27a"
1662 },
1663 {
1664 "path": "styles/dist/types_unprefixed/prefabs/control-bar.scss.d.ts",
1665 "package": "styles",
1666 "sha256": "20a51d608a719340200ec49a42d2aed6e3fd29f6023b8b1c5d58f792ad9fab33",
1667 "upstreamSha256": "20a51d608a719340200ec49a42d2aed6e3fd29f6023b8b1c5d58f792ad9fab33"
1668 },
1669 {
1670 "path": "styles/dist/types_unprefixed/prefabs/index.scss.d.ts",
1671 "package": "styles",
1672 "sha256": "c1ad4125f7babc4e62f8d1ebd6291b1ba0ad489dab52a750303619739d2b076b",
1673 "upstreamSha256": "c1ad4125f7babc4e62f8d1ebd6291b1ba0ad489dab52a750303619739d2b076b"
1674 },
1675 {
1676 "path": "styles/dist/types_unprefixed/prefabs/prejoin.scss.d.ts",
1677 "package": "styles",
1678 "sha256": "ec03a358fde5ae22050d0a78362b65ce5a769636dd430040f0f35eb86fbbf652",
1679 "upstreamSha256": "ec03a358fde5ae22050d0a78362b65ce5a769636dd430040f0f35eb86fbbf652"
1680 },
1681 {
1682 "path": "styles/dist/types_unprefixed/prefabs/video-conference.scss.d.ts",
1683 "package": "styles",
1684 "sha256": "b01eb3840b1cb433fcadeb442445e45866ba6d0e53f5591024b4ddb65e362cbb",
1685 "upstreamSha256": "b01eb3840b1cb433fcadeb442445e45866ba6d0e53f5591024b4ddb65e362cbb"
1686 },
1687 {
1688 "path": "styles/dist/types_unprefixed/themes/huddle.scss.d.ts",
1689 "package": "styles",
1690 "sha256": "d4c1e171211815e58258bf502461d345649f80557ecaf164315fbe4f9aee1420",
1691 "upstreamSha256": "d4c1e171211815e58258bf502461d345649f80557ecaf164315fbe4f9aee1420"
1692 },
1693 {
1694 "path": "styles/dist/types_unprefixed/_preflight.scss.d.ts",
1695 "package": "styles",
1696 "sha256": "c8a3cca3b0ad556348276fd3210ab5f5be57cabb06db98d3e35060f6f64267ac",
1697 "upstreamSha256": "c8a3cca3b0ad556348276fd3210ab5f5be57cabb06db98d3e35060f6f64267ac"
1698 }
1699 ]
1700}