Skip to content

Commit f779352

Browse files
authored
Merge branch 'livekit:main' into fkwp/isSafariSpeakerSelectionSupported
2 parents 833ad22 + b09b800 commit f779352

20 files changed

Lines changed: 2029 additions & 598 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Change Log
22

3+
## 2.19.0
4+
5+
### Minor Changes
6+
7+
- Add new RPC protocol updates to support infinite payload length in requests / responses - [#1832](https://github.com/livekit/client-sdk-js/pull/1832) ([@1egoman](https://github.com/1egoman))
8+
39
## 2.18.10
410

511
### Patch Changes

examples/demo/demo.ts

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
RemoteDataTrack,
77
RoomConnectOptions,
88
RoomOptions,
9+
RpcInvocationData,
910
ScalabilityMode,
1011
SimulationScenario,
1112
VideoCaptureOptions,
@@ -26,6 +27,7 @@ import {
2627
RemoteVideoTrack,
2728
Room,
2829
RoomEvent,
30+
RpcError,
2931
ScreenSharePresets,
3032
Track,
3133
TrackPublication,
@@ -70,6 +72,19 @@ let streamReaderAbortController: AbortController | undefined;
7072
let localDataTracks: Array<LocalDataTrack> = [];
7173
let remoteDataTracks: Array<RemoteDataTrack> = [];
7274

75+
type RpcHandlerEntry = {
76+
topic: string;
77+
reply: string;
78+
invocationCount: number;
79+
};
80+
const rpcHandlers: Map<string, RpcHandlerEntry> = new Map();
81+
82+
const RPC_PRESETS = {
83+
hello: 'hello world',
84+
'20k': 'X'.repeat(20_000),
85+
} as const;
86+
type RpcPresetKey = keyof typeof RPC_PRESETS;
87+
7388
const searchParams = new URLSearchParams(window.location.search);
7489
const storedUrl = searchParams.get('url') ?? 'ws://localhost:7880';
7590
const storedToken = searchParams.get('token') ?? '';
@@ -824,6 +839,74 @@ const appActions = {
824839
button.removeAttribute('disabled');
825840
}
826841
},
842+
843+
fillRpcPreset: (inputId: string, key: RpcPresetKey) => {
844+
const input = <HTMLInputElement>$(inputId);
845+
input.value = RPC_PRESETS[key];
846+
input.dispatchEvent(new Event('input', { bubbles: true }));
847+
},
848+
849+
sendRpc: async () => {
850+
const destinationIdentity = (<HTMLSelectElement>$('rpc-send-destination')).value;
851+
const method = (<HTMLInputElement>$('rpc-send-topic')).value.trim();
852+
const payload = (<HTMLInputElement>$('rpc-send-payload')).value;
853+
const result = $('rpc-send-result');
854+
const button = <HTMLButtonElement>$('rpc-send-button');
855+
if (!currentRoom) {
856+
result.textContent = '✕ Not connected';
857+
result.className = 'text-monospace mt-1 text-danger';
858+
return;
859+
}
860+
if (!destinationIdentity) {
861+
result.textContent = '✕ Choose a destination';
862+
result.className = 'text-monospace mt-1 text-danger';
863+
return;
864+
}
865+
if (!method) {
866+
result.textContent = '✕ Topic is required';
867+
result.className = 'text-monospace mt-1 text-danger';
868+
return;
869+
}
870+
result.textContent = `→ ${destinationIdentity} ${method}...`;
871+
result.className = 'text-monospace mt-1 text-muted';
872+
button.disabled = true;
873+
try {
874+
const response = await currentRoom.localParticipant.performRpc({
875+
destinationIdentity,
876+
method,
877+
payload,
878+
});
879+
const preview =
880+
response.length > 20 ? `${response.slice(0, 20)}... (${response.length}B)` : response;
881+
result.textContent = `✓ ${preview}`;
882+
result.className = 'text-monospace mt-1 text-success';
883+
} catch (err) {
884+
const msg = err instanceof RpcError ? `${err.code}: ${err.message}` : String(err);
885+
result.textContent = `✕ ${msg}`;
886+
result.className = 'text-monospace mt-1 text-danger';
887+
} finally {
888+
button.disabled = false;
889+
}
890+
},
891+
892+
registerRpcHandler: () => {
893+
const topicInput = <HTMLInputElement>$('rpc-handler-topic');
894+
const topic = topicInput.value.trim();
895+
if (!currentRoom || !topic || rpcHandlers.has(topic)) {
896+
topicInput.classList.add('is-invalid');
897+
return;
898+
}
899+
topicInput.classList.remove('is-invalid');
900+
const entry: RpcHandlerEntry = { topic, reply: '', invocationCount: 0 };
901+
rpcHandlers.set(topic, entry);
902+
currentRoom.registerRpcMethod(topic, async (data: RpcInvocationData) => {
903+
entry.invocationCount += 1;
904+
appendRpcInvocation(topic, entry.invocationCount, data.callerIdentity, data.payload);
905+
return entry.reply;
906+
});
907+
topicInput.value = '';
908+
renderRpcHandlers();
909+
},
827910
};
828911

829912
declare global {
@@ -878,12 +961,14 @@ async function participantConnected(participant: Participant) {
878961
.on(ParticipantEvent.ConnectionQualityChanged, () => {
879962
renderParticipant(participant);
880963
});
964+
refreshRpcDestinations();
881965
}
882966

883967
function participantDisconnected(participant: RemoteParticipant) {
884968
appendLog('participant', participant.sid, 'disconnected');
885969

886970
renderParticipant(participant, true);
971+
refreshRpcDestinations();
887972
}
888973

889974
function handleRoomDisconnect(reason?: DisconnectReason) {
@@ -902,6 +987,11 @@ function handleRoomDisconnect(reason?: DisconnectReason) {
902987
remoteDataTracks = [];
903988
renderRemoteDataTracks();
904989

990+
rpcHandlers.clear();
991+
renderRpcHandlers();
992+
$('rpc-send-result').textContent = '';
993+
refreshRpcDestinations();
994+
905995
const container = $('participants-area');
906996
if (container) {
907997
container.innerHTML = '';
@@ -1502,6 +1592,162 @@ function renderRemoteDataTracks() {
15021592
}
15031593
}
15041594

1595+
function createRpcHandlerElement(topic: string): HTMLElement {
1596+
const item = document.createElement('div');
1597+
item.className = 'list-group-item local-data-track-item p-2 mt-2';
1598+
item.dataset.topic = topic;
1599+
1600+
const safeId = encodeURIComponent(topic).replace(/%/g, '_');
1601+
const replyInputId = `rpc-handler-reply-${safeId}`;
1602+
const unregisterId = `rpc-handler-unregister-${safeId}`;
1603+
1604+
item.innerHTML = `
1605+
<div class="d-flex align-items-start justify-content-between mt-1">
1606+
<span class="font-weight-bold text-truncate mr-2" title="${topic}">${topic}</span>
1607+
<button id="${unregisterId}" class="btn btn-outline-danger btn-sm" type="button">✕</button>
1608+
</div>
1609+
<div class="input-group input-group-sm mt-2">
1610+
<input
1611+
id="${replyInputId}"
1612+
type="text"
1613+
class="form-control text-monospace"
1614+
placeholder="Reply payload"
1615+
/>
1616+
<div class="input-group-append">
1617+
<button class="btn btn-outline-secondary rpc-handler-preset-hello" type="button">Hello</button>
1618+
<button class="btn btn-outline-secondary rpc-handler-preset-20k" type="button">20k</button>
1619+
</div>
1620+
</div>
1621+
<div
1622+
class="rpc-handler-well bg-dark rounded p-2 text-monospace mt-2"
1623+
style="max-height: 180px; overflow-y: auto; font-size: 0.7rem; min-height: 60px; display: flex; align-items: center; justify-content: center;"
1624+
>
1625+
<span class="rpc-handler-placeholder text-muted">No invocations yet</span>
1626+
</div>
1627+
`;
1628+
1629+
const replyInput = item.querySelector<HTMLInputElement>(`#${replyInputId}`)!;
1630+
replyInput.addEventListener('input', () => {
1631+
const entry = rpcHandlers.get(topic);
1632+
if (entry) {
1633+
entry.reply = replyInput.value;
1634+
}
1635+
});
1636+
1637+
item
1638+
.querySelector<HTMLButtonElement>('.rpc-handler-preset-hello')!
1639+
.addEventListener('click', () => {
1640+
replyInput.value = RPC_PRESETS.hello;
1641+
const entry = rpcHandlers.get(topic);
1642+
if (entry) {
1643+
entry.reply = replyInput.value;
1644+
}
1645+
});
1646+
item
1647+
.querySelector<HTMLButtonElement>('.rpc-handler-preset-20k')!
1648+
.addEventListener('click', () => {
1649+
replyInput.value = RPC_PRESETS['20k'];
1650+
const entry = rpcHandlers.get(topic);
1651+
if (entry) {
1652+
entry.reply = replyInput.value;
1653+
}
1654+
});
1655+
1656+
const unregisterBtn = item.querySelector<HTMLButtonElement>(`#${unregisterId}`)!;
1657+
unregisterBtn.addEventListener('click', () => {
1658+
if (currentRoom) {
1659+
currentRoom.unregisterRpcMethod(topic);
1660+
}
1661+
rpcHandlers.delete(topic);
1662+
renderRpcHandlers();
1663+
});
1664+
1665+
return item;
1666+
}
1667+
1668+
function renderRpcHandlers() {
1669+
const wrapper = $('rpc-handlers-list');
1670+
const rendered = new Set<string>();
1671+
1672+
for (const child of Array.from(wrapper.children)) {
1673+
const el = child as HTMLElement;
1674+
const topic = el.dataset.topic!;
1675+
if (!rpcHandlers.has(topic)) {
1676+
el.remove();
1677+
} else {
1678+
rendered.add(topic);
1679+
}
1680+
}
1681+
1682+
for (const topic of rpcHandlers.keys()) {
1683+
if (!rendered.has(topic)) {
1684+
wrapper.appendChild(createRpcHandlerElement(topic));
1685+
}
1686+
}
1687+
}
1688+
1689+
function appendRpcInvocation(topic: string, n: number, caller: string, payload: string): void {
1690+
const card = $('rpc-handlers-list').querySelector<HTMLElement>(
1691+
`[data-topic="${CSS.escape(topic)}"]`,
1692+
);
1693+
if (!card) return;
1694+
const well = card.querySelector<HTMLElement>('.rpc-handler-well')!;
1695+
const placeholder = well.querySelector('.rpc-handler-placeholder');
1696+
if (placeholder) {
1697+
placeholder.remove();
1698+
well.style.display = 'block';
1699+
}
1700+
1701+
const entry = document.createElement('div');
1702+
entry.className = 'border-bottom border-secondary pb-1 mb-1';
1703+
1704+
const sizeString =
1705+
payload.length < 1024 ? `${payload.length}B` : `${(payload.length / 1024).toFixed(2)}KB`;
1706+
1707+
const meta = document.createElement('div');
1708+
meta.className = 'text-muted';
1709+
meta.style.cssText = 'font-size: 0.65rem;';
1710+
meta.textContent = `#${n} · ${caller} · ${sizeString} · ${new Date().toISOString()}`;
1711+
entry.appendChild(meta);
1712+
1713+
const body = document.createElement('div');
1714+
body.style.cssText = 'white-space: pre-wrap; word-break: break-word; color: #e9ecef;';
1715+
body.textContent = payload;
1716+
entry.appendChild(body);
1717+
1718+
well.appendChild(entry);
1719+
well.scrollTop = well.scrollHeight;
1720+
}
1721+
1722+
function refreshRpcDestinations(): void {
1723+
const select = <HTMLSelectElement>$('rpc-send-destination');
1724+
const previous = select.value;
1725+
1726+
const identities: string[] = [];
1727+
if (currentRoom) {
1728+
currentRoom.remoteParticipants.forEach((p) => identities.push(p.identity));
1729+
}
1730+
identities.sort();
1731+
1732+
select.innerHTML = '';
1733+
if (identities.length === 0) {
1734+
const opt = document.createElement('option');
1735+
opt.value = '';
1736+
opt.textContent = '(no remote participants)';
1737+
select.appendChild(opt);
1738+
} else {
1739+
for (const identity of identities) {
1740+
const opt = document.createElement('option');
1741+
opt.value = identity;
1742+
opt.textContent = identity;
1743+
select.appendChild(opt);
1744+
}
1745+
if (identities.includes(previous)) {
1746+
select.value = previous;
1747+
}
1748+
}
1749+
}
1750+
15051751
function getParticipantsAreaElement(): HTMLElement {
15061752
return (
15071753
window.documentPictureInPicture?.window?.document.querySelector('#participants-area') ||

examples/demo/index.html

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,84 @@ <h3>Data Tracks</h3>
366366
<div id="data-tracks-remote-list">
367367
<!-- data track entries dynamically inserted in here -->
368368
</div>
369+
370+
<h3 class="mt-4">RPC</h3>
371+
372+
<div class="input-group input-group-sm">
373+
<select
374+
class="form-control text-monospace"
375+
id="rpc-send-destination"
376+
>
377+
<option value="">(no remote participants)</option>
378+
</select>
379+
</div>
380+
<div class="input-group input-group-sm mt-1">
381+
<input
382+
type="text"
383+
class="form-control text-monospace"
384+
id="rpc-send-topic"
385+
placeholder="Topic (method)"
386+
/>
387+
</div>
388+
<div class="input-group input-group-sm mt-1">
389+
<input
390+
type="text"
391+
class="form-control text-monospace"
392+
id="rpc-send-payload"
393+
placeholder="Payload"
394+
/>
395+
<div class="input-group-append">
396+
<button
397+
class="btn btn-outline-secondary"
398+
type="button"
399+
onclick="appActions.fillRpcPreset('rpc-send-payload','hello')"
400+
>
401+
Hello
402+
</button>
403+
<button
404+
class="btn btn-outline-secondary"
405+
type="button"
406+
onclick="appActions.fillRpcPreset('rpc-send-payload','20k')"
407+
>
408+
20k
409+
</button>
410+
<button
411+
class="btn btn-secondary"
412+
type="button"
413+
id="rpc-send-button"
414+
onclick="appActions.sendRpc()"
415+
>
416+
Send
417+
</button>
418+
</div>
419+
</div>
420+
<div
421+
id="rpc-send-result"
422+
class="text-monospace mt-1"
423+
style="font-size: 0.75rem;"
424+
></div>
425+
426+
<div class="input-group input-group-sm mt-3">
427+
<input
428+
type="text"
429+
class="form-control text-monospace"
430+
id="rpc-handler-topic"
431+
placeholder="Topic (method)"
432+
/>
433+
<div class="input-group-append">
434+
<button
435+
class="btn btn-secondary"
436+
type="button"
437+
id="rpc-handler-register-button"
438+
onclick="appActions.registerRpcHandler()"
439+
>
440+
Register Handler
441+
</button>
442+
</div>
443+
</div>
444+
<div id="rpc-handlers-list">
445+
<!-- rpc handler cards dynamically inserted in here -->
446+
</div>
369447
</div>
370448
</div>
371449
</div>

0 commit comments

Comments
 (0)