Skip to content

Commit 1ef5db5

Browse files
RPC implementation via FFI (#283)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 5b78bcc commit 1ef5db5

File tree

6 files changed

+696
-4
lines changed

6 files changed

+696
-4
lines changed

README.md

+45
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,51 @@ def on_message_received(msg: rtc.ChatMessage):
128128
await chat.send_message("hello world")
129129
```
130130

131+
132+
### RPC
133+
134+
Perform your own predefined method calls from one participant to another.
135+
136+
This feature is especially powerful when used with [Agents](https://docs.livekit.io/agents), for instance to forward LLM function calls to your client application.
137+
138+
#### Registering an RPC method
139+
140+
The participant who implements the method and will receive its calls must first register support:
141+
142+
```python
143+
@room.local_participant.register_rpc_method("greet")
144+
async def handle_greet(data: RpcInvocationData):
145+
print(f"Received greeting from {data.caller_identity}: {data.payload}")
146+
return f"Hello, {data.caller_identity}!"
147+
```
148+
149+
In addition to the payload, your handler will also receive `response_timeout`, which informs you the maximum time available to return a response. If you are unable to respond in time, the call will result in an error on the caller's side.
150+
151+
#### Performing an RPC request
152+
153+
The caller may then initiate an RPC call like so:
154+
155+
```python
156+
try:
157+
response = await room.local_participant.perform_rpc(
158+
destination_identity='recipient-identity',
159+
method='greet',
160+
payload='Hello from RPC!'
161+
)
162+
print(f"RPC response: {response}")
163+
except Exception as e:
164+
print(f"RPC call failed: {e}")
165+
```
166+
167+
You may find it useful to adjust the `response_timeout` parameter, which indicates the amount of time you will wait for a response. We recommend keeping this value as low as possible while still satisfying the constraints of your application.
168+
169+
#### Errors
170+
171+
LiveKit is a dynamic realtime environment and calls can fail for various reasons.
172+
173+
You may throw errors of the type `RpcError` with a string `message` in an RPC method handler and they will be received on the caller's side with the message intact. Other errors will not be transmitted and will instead arrive to the caller as `1500` ("Application Error"). Other built-in errors are detailed in `RpcError`.
174+
175+
131176
## Examples
132177

133178
- [Facelandmark](https://github.com/livekit/python-sdks/tree/main/examples/face_landmark): Use mediapipe to detect face landmarks (eyes, nose ...)

examples/rpc.py

+287
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
from livekit import rtc, api
2+
import os
3+
import json
4+
import asyncio
5+
from dotenv import load_dotenv
6+
from livekit.rtc.rpc import RpcInvocationData
7+
8+
load_dotenv(dotenv_path=".env.local", override=False)
9+
LIVEKIT_API_KEY = os.getenv("LIVEKIT_API_KEY")
10+
LIVEKIT_API_SECRET = os.getenv("LIVEKIT_API_SECRET")
11+
LIVEKIT_URL = os.getenv("LIVEKIT_URL")
12+
if not LIVEKIT_API_KEY or not LIVEKIT_API_SECRET or not LIVEKIT_URL:
13+
raise ValueError(
14+
"Missing required environment variables. Please check your .env.local file."
15+
)
16+
17+
18+
async def main():
19+
rooms = [] # Keep track of all rooms for cleanup
20+
try:
21+
room_name = f"rpc-test-{os.urandom(4).hex()}"
22+
print(f"Connecting participants to room: {room_name}")
23+
24+
callers_room, greeters_room, math_genius_room = await asyncio.gather(
25+
connect_participant("caller", room_name),
26+
connect_participant("greeter", room_name),
27+
connect_participant("math-genius", room_name),
28+
)
29+
rooms = [callers_room, greeters_room, math_genius_room]
30+
31+
register_receiver_methods(greeters_room, math_genius_room)
32+
33+
try:
34+
print("\n\nRunning greeting example...")
35+
await asyncio.gather(perform_greeting(callers_room))
36+
except Exception as error:
37+
print("Error:", error)
38+
39+
try:
40+
print("\n\nRunning error handling example...")
41+
await perform_divide(callers_room)
42+
except Exception as error:
43+
print("Error:", error)
44+
45+
try:
46+
print("\n\nRunning math example...")
47+
await perform_square_root(callers_room)
48+
await asyncio.sleep(2)
49+
await perform_quantum_hypergeometric_series(callers_room)
50+
except Exception as error:
51+
print("Error:", error)
52+
53+
try:
54+
print("\n\nRunning long calculation with timeout...")
55+
await asyncio.create_task(perform_long_calculation(callers_room))
56+
except Exception as error:
57+
print("Error:", error)
58+
59+
try:
60+
print("\n\nRunning long calculation with disconnect...")
61+
# Start the long calculation
62+
long_calc_task = asyncio.create_task(perform_long_calculation(callers_room))
63+
# Wait a bit then disconnect the math genius
64+
await asyncio.sleep(5)
65+
print("\nDisconnecting math genius early...")
66+
await math_genius_room.disconnect()
67+
# Wait for the calculation to fail
68+
await long_calc_task
69+
except Exception as error:
70+
print("Error:", error)
71+
72+
print("\n\nParticipants done, disconnecting remaining participants...")
73+
await callers_room.disconnect()
74+
await greeters_room.disconnect()
75+
76+
print("Participants disconnected. Example completed.")
77+
78+
except KeyboardInterrupt:
79+
print("\nReceived interrupt signal, cleaning up...")
80+
except Exception as e:
81+
print(f"Unexpected error: {e}")
82+
finally:
83+
# Clean up all rooms
84+
print("Disconnecting all participants...")
85+
await asyncio.gather(
86+
*(room.disconnect() for room in rooms), return_exceptions=True
87+
)
88+
print("Cleanup complete")
89+
90+
91+
def register_receiver_methods(greeters_room: rtc.Room, math_genius_room: rtc.Room):
92+
@greeters_room.local_participant.register_rpc_method("arrival")
93+
async def arrival_method(
94+
data: RpcInvocationData,
95+
):
96+
print(f'[Greeter] Oh {data.caller_identity} arrived and said "{data.payload}"')
97+
await asyncio.sleep(2)
98+
return "Welcome and have a wonderful day!"
99+
100+
@math_genius_room.local_participant.register_rpc_method("square-root")
101+
async def square_root_method(
102+
data: RpcInvocationData,
103+
):
104+
json_data = json.loads(data.payload)
105+
number = json_data["number"]
106+
print(
107+
f"[Math Genius] I guess {data.caller_identity} wants the square root of {number}. I've only got {data.response_timeout} seconds to respond but I think I can pull it off."
108+
)
109+
110+
print("[Math Genius] *doing math*…")
111+
await asyncio.sleep(2)
112+
113+
result = number**0.5
114+
print(f"[Math Genius] Aha! It's {result}")
115+
return json.dumps({"result": result})
116+
117+
@math_genius_room.local_participant.register_rpc_method("divide")
118+
async def divide_method(
119+
data: RpcInvocationData,
120+
):
121+
json_data = json.loads(data.payload)
122+
dividend = json_data["dividend"]
123+
divisor = json_data["divisor"]
124+
print(
125+
f"[Math Genius] {data.caller_identity} wants to divide {dividend} by {divisor}."
126+
)
127+
128+
result = dividend / divisor
129+
return json.dumps({"result": result})
130+
131+
@math_genius_room.local_participant.register_rpc_method("long-calculation")
132+
async def long_calculation_method(
133+
data: RpcInvocationData,
134+
):
135+
print(
136+
f"[Math Genius] Starting a very long calculation for {data.caller_identity}"
137+
)
138+
print(
139+
f"[Math Genius] This will take 30 seconds even though you're only giving me {data.response_timeout} seconds"
140+
)
141+
await asyncio.sleep(30)
142+
return json.dumps({"result": "Calculation complete!"})
143+
144+
145+
async def perform_greeting(room: rtc.Room):
146+
print("[Caller] Letting the greeter know that I've arrived")
147+
try:
148+
response = await room.local_participant.perform_rpc(
149+
destination_identity="greeter", method="arrival", payload="Hello"
150+
)
151+
print(f'[Caller] That\'s nice, the greeter said: "{response}"')
152+
except Exception as error:
153+
print(f"[Caller] RPC call failed: {error}")
154+
raise
155+
156+
157+
async def perform_square_root(room: rtc.Room):
158+
print("[Caller] What's the square root of 16?")
159+
try:
160+
response = await room.local_participant.perform_rpc(
161+
destination_identity="math-genius",
162+
method="square-root",
163+
payload=json.dumps({"number": 16}),
164+
)
165+
parsed_response = json.loads(response)
166+
print(f"[Caller] Nice, the answer was {parsed_response['result']}")
167+
except Exception as error:
168+
print(f"[Caller] RPC call failed: {error}")
169+
raise
170+
171+
172+
async def perform_quantum_hypergeometric_series(room: rtc.Room):
173+
print("[Caller] What's the quantum hypergeometric series of 42?")
174+
try:
175+
response = await room.local_participant.perform_rpc(
176+
destination_identity="math-genius",
177+
method="quantum-hypergeometric-series",
178+
payload=json.dumps({"number": 42}),
179+
)
180+
parsed_response = json.loads(response)
181+
print(f"[Caller] genius says {parsed_response['result']}!")
182+
except rtc.RpcError as error:
183+
if error.code == rtc.RpcError.ErrorCode.UNSUPPORTED_METHOD:
184+
print("[Caller] Aww looks like the genius doesn't know that one.")
185+
return
186+
print("[Caller] Unexpected error:", error)
187+
raise
188+
except Exception as error:
189+
print("[Caller] Unexpected error:", error)
190+
raise
191+
192+
193+
async def perform_divide(room: rtc.Room):
194+
print("[Caller] Let's divide 10 by 0.")
195+
try:
196+
response = await room.local_participant.perform_rpc(
197+
destination_identity="math-genius",
198+
method="divide",
199+
payload=json.dumps({"dividend": 10, "divisor": 0}),
200+
)
201+
parsed_response = json.loads(response)
202+
print(f"[Caller] The result is {parsed_response['result']}")
203+
except rtc.RpcError as error:
204+
if error.code == rtc.RpcError.ErrorCode.APPLICATION_ERROR:
205+
print(
206+
"[Caller] Aww something went wrong with that one, lets try something else."
207+
)
208+
else:
209+
print(f"[Caller] RPC call failed with unexpected RpcError: {error}")
210+
except Exception as error:
211+
print(f"[Caller] RPC call failed with unexpected error: {error}")
212+
213+
214+
async def perform_long_calculation(room: rtc.Room):
215+
print("[Caller] Giving the math genius 10s to complete a long calculation")
216+
try:
217+
response = await room.local_participant.perform_rpc(
218+
destination_identity="math-genius",
219+
method="long-calculation",
220+
payload=json.dumps({}),
221+
response_timeout=10,
222+
)
223+
parsed_response = json.loads(response)
224+
print(f"[Caller] Result: {parsed_response['result']}")
225+
except rtc.RpcError as error:
226+
if error.code == rtc.RpcError.ErrorCode.RESPONSE_TIMEOUT:
227+
print("[Caller] Math genius took too long to respond")
228+
elif error.code == rtc.RpcError.ErrorCode.RECIPIENT_DISCONNECTED:
229+
print("[Caller] Math genius disconnected before response was received")
230+
else:
231+
print(f"[Caller] Unexpected RPC error: {error}")
232+
except Exception as error:
233+
print(f"[Caller] Unexpected error: {error}")
234+
235+
236+
def create_token(identity: str, room_name: str):
237+
token = (
238+
api.AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET)
239+
.with_identity(identity)
240+
.with_grants(
241+
api.VideoGrants(
242+
room=room_name,
243+
room_join=True,
244+
can_publish=True,
245+
can_subscribe=True,
246+
)
247+
)
248+
)
249+
return token.to_jwt()
250+
251+
252+
async def connect_participant(identity: str, room_name: str) -> rtc.Room:
253+
room = rtc.Room()
254+
token = create_token(identity, room_name)
255+
256+
def on_disconnected(reason: str):
257+
print(f"[{identity}] Disconnected from room: {reason}")
258+
259+
room.on("disconnected", on_disconnected)
260+
261+
await room.connect(LIVEKIT_URL, token)
262+
263+
async def wait_for_participants():
264+
if room.remote_participants:
265+
return
266+
participant_connected = asyncio.Event()
267+
268+
def _on_participant_connected(participant: rtc.RemoteParticipant):
269+
room.off("participant_connected", _on_participant_connected)
270+
participant_connected.set()
271+
272+
room.on("participant_connected", _on_participant_connected)
273+
await participant_connected.wait()
274+
275+
try:
276+
await asyncio.wait_for(wait_for_participants(), timeout=5.0)
277+
except asyncio.TimeoutError:
278+
raise TimeoutError("Timed out waiting for participants")
279+
280+
return room
281+
282+
283+
if __name__ == "__main__":
284+
try:
285+
asyncio.run(main())
286+
except KeyboardInterrupt:
287+
print("\nProgram terminated by user")

livekit-rtc/livekit/rtc/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
from .video_stream import VideoFrameEvent, VideoStream
7373
from .audio_resampler import AudioResampler, AudioResamplerQuality
7474
from .utils import combine_audio_frames
75+
from .rpc import RpcError, RpcInvocationData
7576

7677
__all__ = [
7778
"ConnectionQuality",
@@ -132,6 +133,8 @@
132133
"ChatMessage",
133134
"AudioResampler",
134135
"AudioResamplerQuality",
136+
"RpcError",
137+
"RpcInvocationData",
135138
"EventEmitter",
136139
"combine_audio_frames",
137140
"__version__",

0 commit comments

Comments
 (0)