Skip to content

Commit e395c06

Browse files
Improve API ergonomics, updated examples and readme (#91)
Co-authored-by: Théo Monnom <[email protected]>
1 parent 69fd128 commit e395c06

File tree

12 files changed

+262
-157
lines changed

12 files changed

+262
-157
lines changed

README.md

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,72 @@
88

99
[![pypi-v](https://img.shields.io/pypi/v/livekit.svg)](https://pypi.org/project/livekit/)
1010

11-
# 📹🎙️🐍 Python Client SDK for LiveKit
11+
# 📹🎙️🐍 Python SDK for LiveKit
1212

13-
The Livekit Python Client provides a convenient interface for integrating Livekit's real-time video and audio capabilities into your Python applications. With this library, developers can easily leverage Livekit's WebRTC functionalities, allowing them to focus on building their AI models or other application logic without worrying about the complexities of WebRTC.
13+
<!--BEGIN_DESCRIPTION-->
1414

15-
Official LiveKit documentation: https://docs.livekit.io/
15+
The LiveKit Python SDK provides a convenient interface for integrating LiveKit's real-time video and audio capabilities into your Python applications. With it, developers can easily leverage LiveKit's WebRTC functionalities, allowing them to focus on building their AI models or other application logic without worrying about the complexities of WebRTC.
1616

17-
## Installation
17+
<!--END_DESCRIPTION-->
18+
19+
This repo contains two packages
20+
21+
- [livekit](https://pypi.org/project/livekit/): Real-time SDK for connecting to LiveKit as a participant
22+
- [livekit-api](https://pypi.org/project/livekit-api/): Access token generation and server APIs
23+
24+
## Using Server API
1825

19-
RTC Client:
2026
```shell
21-
$ pip install livekit
27+
$ pip install livekit-api
28+
```
29+
30+
### Generating an access token
31+
32+
```python
33+
from livekit import api
34+
import os
35+
36+
token = api.AccessToken(os.getenv('LIVEKIT_API_KEY'), os.getenv('LIVEKIT_API_SECRET')) \
37+
.with_identity("python-bot") \
38+
.with_name("Python Bot") \
39+
.with_grants(api.VideoGrants(
40+
room_join=True,
41+
room="my-room",
42+
)).to_jwt()
2243
```
2344

24-
API / Server SDK:
45+
### Creating a room
46+
47+
RoomService uses asyncio and aiohttp to make API calls. It needs to be used with an event loop.
48+
49+
```python
50+
from livekit import api
51+
import asyncio
52+
53+
async def main():
54+
room_service = api.RoomService(
55+
'http://localhost:7880',
56+
'devkey',
57+
'secret',
58+
)
59+
room_info = await room_service.create_room(
60+
api.room.CreateRoomRequest(name="my-room"),
61+
)
62+
print(room_info)
63+
results = await room_service.list_rooms(api.room.ListRoomsRequest())
64+
print(results)
65+
await room_service.aclose()
66+
67+
asyncio.get_event_loop().run_until_complete(main())
68+
```
69+
70+
## Using Real-time SDK
71+
2572
```shell
26-
$ pip install livekit-api
73+
$ pip install livekit
2774
```
2875

29-
## Connecting to a room
76+
### Connecting to a room
3077

3178
```python
3279
from livekit import rtc
@@ -64,21 +111,6 @@ async def main():
64111
print("track publication: %s", publication.sid)
65112
```
66113

67-
## Create a new access token
68-
69-
```python
70-
from livekit import api
71-
72-
token = api.AccessToken("API_KEY", "SECRET_KEY")
73-
token = AccessToken()
74-
jwt = (
75-
token.with_identity("user1")
76-
.with_name("user1")
77-
.with_grants(VideoGrants(room_join=True, room="room1"))
78-
.to_jwt()
79-
)
80-
```
81-
82114
## Examples
83115

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

examples/basic_room.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
import logging
33
from signal import SIGINT, SIGTERM
44
from typing import Union
5+
import os
56

6-
from livekit import rtc
7+
from livekit import api, rtc
78

8-
URL = "ws://localhost:7880"
9-
TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5MDY2MTMyODgsImlzcyI6IkFQSVRzRWZpZFpqclFvWSIsIm5hbWUiOiJuYXRpdmUiLCJuYmYiOjE2NzI2MTMyODgsInN1YiI6Im5hdGl2ZSIsInZpZGVvIjp7InJvb20iOiJ0ZXN0Iiwicm9vbUFkbWluIjp0cnVlLCJyb29tQ3JlYXRlIjp0cnVlLCJyb29tSm9pbiI6dHJ1ZSwicm9vbUxpc3QiOnRydWV9fQ.uSNIangMRu8jZD5mnRYoCHjcsQWCrJXgHCs0aNIgBFY" # noqa
9+
# ensure LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET are set
1010

1111

1212
async def main(room: rtc.Room) -> None:
@@ -127,7 +127,19 @@ def on_reconnecting() -> None:
127127
def on_reconnected() -> None:
128128
logging.info("reconnected")
129129

130-
await room.connect(URL, TOKEN)
130+
token = (
131+
api.AccessToken()
132+
.with_identity("python-bot")
133+
.with_name("Python Bot")
134+
.with_grants(
135+
api.VideoGrants(
136+
room_join=True,
137+
room="my-room",
138+
)
139+
)
140+
.to_jwt()
141+
)
142+
await room.connect(os.getenv("LIVEKIT_URL"), token)
131143
logging.info("connected to room %s", room.name)
132144
logging.info("participants: %s", room.participants)
133145

examples/face_landmark/face_landmark.py

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99
from mediapipe import solutions
1010
from mediapipe.framework.formats import landmark_pb2
1111

12-
from livekit import rtc
12+
from livekit import api, rtc
1313

14-
URL = "ws://localhost:7880"
15-
TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5MDY2MTMyODgsImlzcyI6IkFQSVRzRWZpZFpqclFvWSIsIm5hbWUiOiJuYXRpdmUiLCJuYmYiOjE2NzI2MTMyODgsInN1YiI6Im5hdGl2ZSIsInZpZGVvIjp7InJvb20iOiJ0ZXN0Iiwicm9vbUFkbWluIjp0cnVlLCJyb29tQ3JlYXRlIjp0cnVlLCJyb29tSm9pbiI6dHJ1ZSwicm9vbUxpc3QiOnRydWV9fQ.uSNIangMRu8jZD5mnRYoCHjcsQWCrJXgHCs0aNIgBFY" # noqa
14+
# ensure LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET are set
1615

1716
tasks = set()
1817

@@ -30,10 +29,41 @@
3029
running_mode=VisionRunningMode.VIDEO,
3130
)
3231

33-
# from https://github.com/googlesamples/mediapipe/blob/main/examples/face_landmarker/python/%5BMediaPipe_Python_Tasks%5D_Face_Landmarker.ipynb
32+
33+
async def main(room: rtc.Room) -> None:
34+
video_stream = None
35+
36+
@room.on("track_subscribed")
37+
def on_track_subscribed(track: rtc.Track, *_):
38+
if track.kind == rtc.TrackKind.KIND_VIDEO:
39+
nonlocal video_stream
40+
if video_stream is not None:
41+
# only process the first stream received
42+
return
43+
44+
print("subscribed to track: " + track.name)
45+
video_stream = rtc.VideoStream(track)
46+
task = asyncio.create_task(frame_loop(video_stream))
47+
tasks.add(task)
48+
task.add_done_callback(tasks.remove)
49+
50+
token = (
51+
api.AccessToken()
52+
.with_identity("python-bot")
53+
.with_name("Python Bot")
54+
.with_grants(
55+
api.VideoGrants(
56+
room_join=True,
57+
room="my-room",
58+
)
59+
)
60+
)
61+
await room.connect(os.getenv("LIVEKIT_URL"), token.to_jwt())
62+
print("connected to room: " + room.name)
3463

3564

3665
def draw_landmarks_on_image(rgb_image, detection_result):
66+
# from https://github.com/googlesamples/mediapipe/blob/main/examples/face_landmarker/python/%5BMediaPipe_Python_Tasks%5D_Face_Landmarker.ipynb
3767
face_landmarks_list = detection_result.face_landmarks
3868

3969
# Loop through the detected faces to visualize.
@@ -111,27 +141,6 @@ async def frame_loop(video_stream: rtc.VideoStream) -> None:
111141
cv2.destroyAllWindows()
112142

113143

114-
async def main(room: rtc.Room) -> None:
115-
video_stream = None
116-
117-
@room.on("track_subscribed")
118-
def on_track_subscribed(track: rtc.Track, *_):
119-
if track.kind == rtc.TrackKind.KIND_VIDEO:
120-
nonlocal video_stream
121-
if video_stream is not None:
122-
# only process the first stream received
123-
return
124-
125-
print("subscribed to track: " + track.name)
126-
video_stream = rtc.VideoStream(track)
127-
task = asyncio.create_task(frame_loop(video_stream))
128-
tasks.add(task)
129-
task.add_done_callback(tasks.remove)
130-
131-
await room.connect(URL, TOKEN)
132-
print("connected to room: " + room.name)
133-
134-
135144
if __name__ == "__main__":
136145
logging.basicConfig(
137146
level=logging.INFO,

examples/publish_hue.py

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,51 @@
11
import asyncio
22
import colorsys
33
import logging
4+
import os
45
from signal import SIGINT, SIGTERM
56

67
import numpy as np
7-
from livekit import rtc
8-
9-
URL = "ws://localhost:7880"
10-
TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5MDY2MTMyODgsImlzcyI6IkFQSVRzRWZpZFpqclFvWSIsIm5hbWUiOiJuYXRpdmUiLCJuYmYiOjE2NzI2MTMyODgsInN1YiI6Im5hdGl2ZSIsInZpZGVvIjp7InJvb20iOiJ0ZXN0Iiwicm9vbUFkbWluIjp0cnVlLCJyb29tQ3JlYXRlIjp0cnVlLCJyb29tSm9pbiI6dHJ1ZSwicm9vbUxpc3QiOnRydWV9fQ.uSNIangMRu8jZD5mnRYoCHjcsQWCrJXgHCs0aNIgBFY" # noqa
8+
from livekit import api, rtc
119

1210
WIDTH, HEIGHT = 1280, 720
1311

1412

13+
# ensure LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET are set
14+
15+
16+
async def main(room: rtc.Room):
17+
token = (
18+
api.AccessToken()
19+
.with_identity("python-publisher")
20+
.with_name("Python Publisher")
21+
.with_grants(
22+
api.VideoGrants(
23+
room_join=True,
24+
room="my-room",
25+
)
26+
)
27+
.to_jwt()
28+
)
29+
url = os.getenv("LIVEKIT_URL")
30+
logging.info("connecting to %s", url)
31+
try:
32+
await room.connect(url, token)
33+
logging.info("connected to room %s", room.name)
34+
except rtc.ConnectError as e:
35+
logging.error("failed to connect to the room: %s", e)
36+
return
37+
38+
# publish a track
39+
source = rtc.VideoSource(WIDTH, HEIGHT)
40+
track = rtc.LocalVideoTrack.create_video_track("hue", source)
41+
options = rtc.TrackPublishOptions()
42+
options.source = rtc.TrackSource.SOURCE_CAMERA
43+
publication = await room.local_participant.publish_track(track, options)
44+
logging.info("published track %s", publication.sid)
45+
46+
asyncio.ensure_future(draw_color_cycle(source))
47+
48+
1549
async def draw_color_cycle(source: rtc.VideoSource):
1650
argb_frame = rtc.ArgbFrame.create(rtc.VideoFormatType.FORMAT_ARGB, WIDTH, HEIGHT)
1751
arr = np.frombuffer(argb_frame.data, dtype=np.uint8)
@@ -42,26 +76,6 @@ async def draw_color_cycle(source: rtc.VideoSource):
4276
await asyncio.sleep(1 / 30 - code_duration)
4377

4478

45-
async def main(room: rtc.Room):
46-
logging.info("connecting to %s", URL)
47-
try:
48-
await room.connect(URL, TOKEN)
49-
logging.info("connected to room %s", room.name)
50-
except rtc.ConnectError as e:
51-
logging.error("failed to connect to the room: %s", e)
52-
return
53-
54-
# publish a track
55-
source = rtc.VideoSource(WIDTH, HEIGHT)
56-
track = rtc.LocalVideoTrack.create_video_track("hue", source)
57-
options = rtc.TrackPublishOptions()
58-
options.source = rtc.TrackSource.SOURCE_CAMERA
59-
publication = await room.local_participant.publish_track(track, options)
60-
logging.info("published track %s", publication.sid)
61-
62-
asyncio.ensure_future(draw_color_cycle(source))
63-
64-
6579
if __name__ == "__main__":
6680
logging.basicConfig(
6781
level=logging.INFO,

examples/publish_wave.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,41 @@
11
import asyncio
22
import logging
33
from signal import SIGINT, SIGTERM
4+
import os
45

56
import numpy as np
6-
from livekit import rtc
7-
8-
URL = "ws://localhost:7880"
9-
TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5MDY2MTMyODgsImlzcyI6IkFQSVRzRWZpZFpqclFvWSIsIm5hbWUiOiJuYXRpdmUiLCJuYmYiOjE2NzI2MTMyODgsInN1YiI6Im5hdGl2ZSIsInZpZGVvIjp7InJvb20iOiJ0ZXN0Iiwicm9vbUFkbWluIjp0cnVlLCJyb29tQ3JlYXRlIjp0cnVlLCJyb29tSm9pbiI6dHJ1ZSwicm9vbUxpc3QiOnRydWV9fQ.uSNIangMRu8jZD5mnRYoCHjcsQWCrJXgHCs0aNIgBFY" # noqa
7+
from livekit import rtc, api
108

119
SAMPLE_RATE = 48000
1210
NUM_CHANNELS = 1
1311

14-
15-
async def publish_frames(source: rtc.AudioSource, frequency: int):
16-
amplitude = 32767 # for 16-bit audio
17-
samples_per_channel = 480 # 10ms at 48kHz
18-
time = np.arange(samples_per_channel) / SAMPLE_RATE
19-
total_samples = 0
20-
audio_frame = rtc.AudioFrame.create(SAMPLE_RATE, NUM_CHANNELS, samples_per_channel)
21-
audio_data = np.frombuffer(audio_frame.data, dtype=np.int16)
22-
while True:
23-
time = (total_samples + np.arange(samples_per_channel)) / SAMPLE_RATE
24-
sine_wave = (amplitude * np.sin(2 * np.pi * frequency * time)).astype(np.int16)
25-
np.copyto(audio_data, sine_wave)
26-
await source.capture_frame(audio_frame)
27-
total_samples += samples_per_channel
12+
# ensure LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET are set
2813

2914

3015
async def main(room: rtc.Room) -> None:
3116
@room.on("participant_disconnected")
3217
def on_participant_disconnect(participant: rtc.Participant, *_):
3318
logging.info("participant disconnected: %s", participant.identity)
3419

35-
logging.info("connecting to %s", URL)
20+
token = (
21+
api.AccessToken()
22+
.with_identity("python-publisher")
23+
.with_name("Python Publisher")
24+
.with_grants(
25+
api.VideoGrants(
26+
room_join=True,
27+
room="my-room",
28+
)
29+
)
30+
.to_jwt()
31+
)
32+
url = os.getenv("LIVEKIT_URL")
33+
34+
logging.info("connecting to %s", url)
3635
try:
3736
await room.connect(
38-
URL,
39-
TOKEN,
37+
url,
38+
token,
4039
options=rtc.RoomOptions(
4140
auto_subscribe=True,
4241
),
@@ -57,6 +56,21 @@ def on_participant_disconnect(participant: rtc.Participant, *_):
5756
asyncio.ensure_future(publish_frames(source, 440))
5857

5958

59+
async def publish_frames(source: rtc.AudioSource, frequency: int):
60+
amplitude = 32767 # for 16-bit audio
61+
samples_per_channel = 480 # 10ms at 48kHz
62+
time = np.arange(samples_per_channel) / SAMPLE_RATE
63+
total_samples = 0
64+
audio_frame = rtc.AudioFrame.create(SAMPLE_RATE, NUM_CHANNELS, samples_per_channel)
65+
audio_data = np.frombuffer(audio_frame.data, dtype=np.int16)
66+
while True:
67+
time = (total_samples + np.arange(samples_per_channel)) / SAMPLE_RATE
68+
sine_wave = (amplitude * np.sin(2 * np.pi * frequency * time)).astype(np.int16)
69+
np.copyto(audio_data, sine_wave)
70+
await source.capture_frame(audio_frame)
71+
total_samples += samples_per_channel
72+
73+
6074
if __name__ == "__main__":
6175
logging.basicConfig(
6276
level=logging.INFO,

0 commit comments

Comments
 (0)