From f117e1840d1d24901db953dd3d24c2c0f02cf271 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Fri, 10 Nov 2023 17:19:26 -0800 Subject: [PATCH 1/7] Improve API ergonomics, updated examples and readme. --- README.md | 82 +++++++++++++++------- examples/publish_hue.py | 65 ++++++++++------- examples/publish_wave.py | 58 +++++++++------ livekit-api/README.md | 3 + livekit-api/livekit/api/__init__.py | 10 +-- livekit-api/livekit/api/connection_info.py | 22 ++++++ livekit-api/livekit/api/room_service.py | 4 +- livekit-api/livekit/api/version.py | 2 +- livekit-rtc/README.md | 4 +- 9 files changed, 168 insertions(+), 82 deletions(-) create mode 100644 livekit-api/livekit/api/connection_info.py diff --git a/README.md b/README.md index 319ffe60..7ac60ce0 100644 --- a/README.md +++ b/README.md @@ -8,25 +8,74 @@ [![pypi-v](https://img.shields.io/pypi/v/livekit.svg)](https://pypi.org/project/livekit/) -# πŸ“ΉπŸŽ™οΈπŸ Python Client SDK for LiveKit +# πŸ“ΉπŸŽ™οΈπŸ Python SDK for LiveKit -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. + -Official LiveKit documentation: https://docs.livekit.io/ +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. -## Installation + + +This repo contains two packages + +- [livekit](https://pypi.org/project/livekit/): Real-time SDK for connecting to LiveKit as a participant +- [livekit-api](https://pypi.org/project/livekit-api/): Access token generation and server APIs + +## Using Server API -RTC Client: ```shell -$ pip install livekit +$ pip install livekit-api +``` + +### Generating an access token + +```python +from livekit import api + +token = api.AccessToken(info.api_key, info.api_secret) \ + .with_identity("python-bot") \ + .with_name("Python Bot") \ + .with_grants(api.VideoGrants( + room_join=True, + room="my-room", + )).to_jwt() +``` + +### Creating a room + +RoomService uses asyncio and aiohttp to make API calls. It needs to be used with an event loop. + +```python +from livekit import api +import asyncio + +async def main(): + # loads connection info from environment variables + # LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET + info = api.ConnectionInfo() + room_service = api.RoomService( + info.http_url(), + info.api_key, + info.api_secret, + ) + room_info = await room_service.create_room( + api.room.CreateRoomRequest(name="my-room"), + ) + print(room_info) + results = await room_service.list_rooms(api.room.ListRoomsRequest()) + print(results) + await room_service.aclose() + +asyncio.get_event_loop().run_until_complete(main()) ``` -API / Server SDK: +## Using Real-time SDK + ```shell -$ pip install livekit-api +$ pip install livekit ``` -## Connecting to a room +### Connecting to a room ```python from livekit import rtc @@ -64,21 +113,6 @@ async def main(): print("track publication: %s", publication.sid) ``` -## Create a new access token - -```python -from livekit import api - -token = api.AccessToken("API_KEY", "SECRET_KEY") -token = AccessToken() -jwt = ( - token.with_identity("user1") - .with_name("user1") - .with_grants(VideoGrants(room_join=True, room="room1")) - .to_jwt() -) -``` - ## Examples - [Facelandmark](https://github.com/livekit/client-sdk-python/tree/main/examples/face_landmark): Use mediapipe to detect face landmarks (eyes, nose ...) diff --git a/examples/publish_hue.py b/examples/publish_hue.py index abc313ca..a91ac741 100644 --- a/examples/publish_hue.py +++ b/examples/publish_hue.py @@ -4,16 +4,45 @@ from signal import SIGINT, SIGTERM import numpy as np -from livekit import rtc - -URL = "ws://localhost:7880" -TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5MDY2MTMyODgsImlzcyI6IkFQSVRzRWZpZFpqclFvWSIsIm5hbWUiOiJuYXRpdmUiLCJuYmYiOjE2NzI2MTMyODgsInN1YiI6Im5hdGl2ZSIsInZpZGVvIjp7InJvb20iOiJ0ZXN0Iiwicm9vbUFkbWluIjp0cnVlLCJyb29tQ3JlYXRlIjp0cnVlLCJyb29tSm9pbiI6dHJ1ZSwicm9vbUxpc3QiOnRydWV9fQ.uSNIangMRu8jZD5mnRYoCHjcsQWCrJXgHCs0aNIgBFY" # noqa +from livekit import api, rtc WIDTH, HEIGHT = 1280, 720 +# ensure LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET are set + + +async def main(room: rtc.Room): + info = api.ConnectionInfo() + token = api.AccessToken(info.api_key, info.api_secret) \ + .with_identity("python-publisher") \ + .with_name("Python Publisher") \ + .with_grants(api.VideoGrants( + room_join=True, + room="my-room", + )).to_jwt() + logging.info("connecting to %s", info.websocket_url()) + try: + await room.connect(info.websocket_url(), token) + logging.info("connected to room %s", room.name) + except rtc.ConnectError as e: + logging.error("failed to connect to the room: %s", e) + return + + # publish a track + source = rtc.VideoSource(WIDTH, HEIGHT) + track = rtc.LocalVideoTrack.create_video_track("hue", source) + options = rtc.TrackPublishOptions() + options.source = rtc.TrackSource.SOURCE_CAMERA + publication = await room.local_participant.publish_track(track, options) + logging.info("published track %s", publication.sid) + + asyncio.ensure_future(draw_color_cycle(source)) + + async def draw_color_cycle(source: rtc.VideoSource): - argb_frame = rtc.ArgbFrame.create(rtc.VideoFormatType.FORMAT_ARGB, WIDTH, HEIGHT) + argb_frame = rtc.ArgbFrame.create( + rtc.VideoFormatType.FORMAT_ARGB, WIDTH, HEIGHT) arr = np.frombuffer(argb_frame.data, dtype=np.uint8) framerate = 1 / 30 @@ -42,30 +71,11 @@ async def draw_color_cycle(source: rtc.VideoSource): await asyncio.sleep(1 / 30 - code_duration) -async def main(room: rtc.Room): - logging.info("connecting to %s", URL) - try: - await room.connect(URL, TOKEN) - logging.info("connected to room %s", room.name) - except rtc.ConnectError as e: - logging.error("failed to connect to the room: %s", e) - return - - # publish a track - source = rtc.VideoSource(WIDTH, HEIGHT) - track = rtc.LocalVideoTrack.create_video_track("hue", source) - options = rtc.TrackPublishOptions() - options.source = rtc.TrackSource.SOURCE_CAMERA - publication = await room.local_participant.publish_track(track, options) - logging.info("published track %s", publication.sid) - - asyncio.ensure_future(draw_color_cycle(source)) - - if __name__ == "__main__": logging.basicConfig( level=logging.INFO, - handlers=[logging.FileHandler("publish_hue.log"), logging.StreamHandler()], + handlers=[logging.FileHandler( + "publish_hue.log"), logging.StreamHandler()], ) loop = asyncio.get_event_loop() @@ -77,7 +87,8 @@ async def cleanup(): asyncio.ensure_future(main(room)) for signal in [SIGINT, SIGTERM]: - loop.add_signal_handler(signal, lambda: asyncio.ensure_future(cleanup())) + loop.add_signal_handler( + signal, lambda: asyncio.ensure_future(cleanup())) try: loop.run_forever() diff --git a/examples/publish_wave.py b/examples/publish_wave.py index 10e4c928..56b5c489 100644 --- a/examples/publish_wave.py +++ b/examples/publish_wave.py @@ -3,28 +3,12 @@ from signal import SIGINT, SIGTERM import numpy as np -from livekit import rtc - -URL = "ws://localhost:7880" -TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5MDY2MTMyODgsImlzcyI6IkFQSVRzRWZpZFpqclFvWSIsIm5hbWUiOiJuYXRpdmUiLCJuYmYiOjE2NzI2MTMyODgsInN1YiI6Im5hdGl2ZSIsInZpZGVvIjp7InJvb20iOiJ0ZXN0Iiwicm9vbUFkbWluIjp0cnVlLCJyb29tQ3JlYXRlIjp0cnVlLCJyb29tSm9pbiI6dHJ1ZSwicm9vbUxpc3QiOnRydWV9fQ.uSNIangMRu8jZD5mnRYoCHjcsQWCrJXgHCs0aNIgBFY" # noqa +from livekit import rtc, api SAMPLE_RATE = 48000 NUM_CHANNELS = 1 - -async def publish_frames(source: rtc.AudioSource, frequency: int): - amplitude = 32767 # for 16-bit audio - samples_per_channel = 480 # 10ms at 48kHz - time = np.arange(samples_per_channel) / SAMPLE_RATE - total_samples = 0 - audio_frame = rtc.AudioFrame.create(SAMPLE_RATE, NUM_CHANNELS, samples_per_channel) - audio_data = np.frombuffer(audio_frame.data, dtype=np.int16) - while True: - time = (total_samples + np.arange(samples_per_channel)) / SAMPLE_RATE - sine_wave = (amplitude * np.sin(2 * np.pi * frequency * time)).astype(np.int16) - np.copyto(audio_data, sine_wave) - await source.capture_frame(audio_frame) - total_samples += samples_per_channel +# ensure LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET are set async def main(room: rtc.Room) -> None: @@ -32,11 +16,20 @@ async def main(room: rtc.Room) -> None: def on_participant_disconnect(participant: rtc.Participant, *_): logging.info("participant disconnected: %s", participant.identity) - logging.info("connecting to %s", URL) + info = api.ConnectionInfo() + token = api.AccessToken(info.api_key, info.api_secret) \ + .with_identity("python-publisher") \ + .with_name("Python Publisher") \ + .with_grants(api.VideoGrants( + room_join=True, + room="my-room", + )).to_jwt() + + logging.info("connecting to %s", info.websocket_url()) try: await room.connect( - URL, - TOKEN, + info.websocket_url(), + token, options=rtc.RoomOptions( auto_subscribe=True, ), @@ -57,10 +50,28 @@ def on_participant_disconnect(participant: rtc.Participant, *_): asyncio.ensure_future(publish_frames(source, 440)) +async def publish_frames(source: rtc.AudioSource, frequency: int): + amplitude = 32767 # for 16-bit audio + samples_per_channel = 480 # 10ms at 48kHz + time = np.arange(samples_per_channel) / SAMPLE_RATE + total_samples = 0 + audio_frame = rtc.AudioFrame.create( + SAMPLE_RATE, NUM_CHANNELS, samples_per_channel) + audio_data = np.frombuffer(audio_frame.data, dtype=np.int16) + while True: + time = (total_samples + np.arange(samples_per_channel)) / SAMPLE_RATE + sine_wave = (amplitude * np.sin(2 * np.pi * + frequency * time)).astype(np.int16) + np.copyto(audio_data, sine_wave) + await source.capture_frame(audio_frame) + total_samples += samples_per_channel + + if __name__ == "__main__": logging.basicConfig( level=logging.INFO, - handlers=[logging.FileHandler("publish_wave.log"), logging.StreamHandler()], + handlers=[logging.FileHandler( + "publish_wave.log"), logging.StreamHandler()], ) loop = asyncio.get_event_loop() @@ -72,7 +83,8 @@ async def cleanup(): asyncio.ensure_future(main(room)) for signal in [SIGINT, SIGTERM]: - loop.add_signal_handler(signal, lambda: asyncio.ensure_future(cleanup())) + loop.add_signal_handler( + signal, lambda: asyncio.ensure_future(cleanup())) try: loop.run_forever() diff --git a/livekit-api/README.md b/livekit-api/README.md index e69de29b..c4b77002 100644 --- a/livekit-api/README.md +++ b/livekit-api/README.md @@ -0,0 +1,3 @@ +# LiveKit Server APIs + +Access LiveKit server APIs and generate access tokens. diff --git a/livekit-api/livekit/api/__init__.py b/livekit-api/livekit/api/__init__.py index 44c528e7..06de959d 100644 --- a/livekit-api/livekit/api/__init__.py +++ b/livekit-api/livekit/api/__init__.py @@ -16,11 +16,13 @@ """ # flake8: noqa -from livekit.protocol.egress import * -from livekit.protocol.ingress import * -from livekit.protocol.models import * -from livekit.protocol.room import * +# re-export packages from protocol +from livekit.protocol import egress +from livekit.protocol import ingress +from livekit.protocol import models +from livekit.protocol import room +from .connection_info import ConnectionInfo from .access_token import VideoGrants, AccessToken from .room_service import RoomService from .version import __version__ diff --git a/livekit-api/livekit/api/connection_info.py b/livekit-api/livekit/api/connection_info.py new file mode 100644 index 00000000..7e5cca70 --- /dev/null +++ b/livekit-api/livekit/api/connection_info.py @@ -0,0 +1,22 @@ +import os + + +class ConnectionInfo: + def __init__(self): + self.url = os.getenv("LIVEKIT_URL", "ws://localhost:7880") + self.api_key = os.getenv("LIVEKIT_API_KEY") + self.api_secret = os.getenv("LIVEKIT_API_SECRET") + + if not self.api_key or not self.api_secret: + raise ValueError( + "LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set") + + def websocket_url(self) -> str: + if self.url.startswith("http"): + return self.url.replace("http", "ws", 1) + return self.url + + def http_url(self) -> str: + if self.url.startswith("ws"): + return self.url.replace("ws", "http", 1) + return self.url diff --git a/livekit-api/livekit/api/room_service.py b/livekit-api/livekit/api/room_service.py index 768201ad..6be0df92 100644 --- a/livekit-api/livekit/api/room_service.py +++ b/livekit-api/livekit/api/room_service.py @@ -7,8 +7,8 @@ class RoomService(Service): - def __init__(self, host: str, api_key: str, api_secret: str): - super().__init__(host, api_key, api_secret) + def __init__(self, url: str, api_key: str, api_secret: str): + super().__init__(url, api_key, api_secret) async def create_room( self, create: proto_room.CreateRoomRequest diff --git a/livekit-api/livekit/api/version.py b/livekit-api/livekit/api/version.py index ae736254..bbab0242 100644 --- a/livekit-api/livekit/api/version.py +++ b/livekit-api/livekit/api/version.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.4" diff --git a/livekit-rtc/README.md b/livekit-rtc/README.md index d552dede..105e723f 100644 --- a/livekit-rtc/README.md +++ b/livekit-rtc/README.md @@ -1 +1,3 @@ -# livekit-rtc +# LiveKit Real-time Python SDK + +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. From c31199bc895d2855da225de00a0a5c045b549906 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Fri, 10 Nov 2023 17:34:09 -0800 Subject: [PATCH 2/7] update remaining examples --- examples/basic_room.py | 24 ++++++--- examples/face_landmark/face_landmark.py | 65 ++++++++++++++----------- 2 files changed, 54 insertions(+), 35 deletions(-) diff --git a/examples/basic_room.py b/examples/basic_room.py index 3859f755..7e898c30 100644 --- a/examples/basic_room.py +++ b/examples/basic_room.py @@ -3,10 +3,9 @@ from signal import SIGINT, SIGTERM from typing import Union -from livekit import rtc +from livekit import api, rtc -URL = "ws://localhost:7880" -TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5MDY2MTMyODgsImlzcyI6IkFQSVRzRWZpZFpqclFvWSIsIm5hbWUiOiJuYXRpdmUiLCJuYmYiOjE2NzI2MTMyODgsInN1YiI6Im5hdGl2ZSIsInZpZGVvIjp7InJvb20iOiJ0ZXN0Iiwicm9vbUFkbWluIjp0cnVlLCJyb29tQ3JlYXRlIjp0cnVlLCJyb29tSm9pbiI6dHJ1ZSwicm9vbUxpc3QiOnRydWV9fQ.uSNIangMRu8jZD5mnRYoCHjcsQWCrJXgHCs0aNIgBFY" # noqa +# ensure LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET are set async def main(room: rtc.Room) -> None: @@ -105,7 +104,8 @@ def on_connection_quality_changed( def on_track_subscription_failed( participant: rtc.RemoteParticipant, track_sid: str, error: str ): - logging.info("track subscription failed: %s %s", participant.identity, error) + logging.info("track subscription failed: %s %s", + participant.identity, error) @room.on("connection_state_changed") def on_connection_state_changed(state: rtc.ConnectionState): @@ -127,7 +127,15 @@ def on_reconnecting() -> None: def on_reconnected() -> None: logging.info("reconnected") - await room.connect(URL, TOKEN) + info = api.ConnectionInfo() + token = api.AccessToken(info.api_key, info.api_secret) \ + .with_identity("python-bot") \ + .with_name("Python Bot") \ + .with_grants(api.VideoGrants( + room_join=True, + room="my-room", + )).to_jwt() + await room.connect(info.websocket_url(), token) logging.info("connected to room %s", room.name) logging.info("participants: %s", room.participants) @@ -138,7 +146,8 @@ def on_reconnected() -> None: if __name__ == "__main__": logging.basicConfig( level=logging.INFO, - handlers=[logging.FileHandler("basic_room.log"), logging.StreamHandler()], + handlers=[logging.FileHandler( + "basic_room.log"), logging.StreamHandler()], ) loop = asyncio.get_event_loop() @@ -150,7 +159,8 @@ async def cleanup(): asyncio.ensure_future(main(room)) for signal in [SIGINT, SIGTERM]: - loop.add_signal_handler(signal, lambda: asyncio.ensure_future(cleanup())) + loop.add_signal_handler( + signal, lambda: asyncio.ensure_future(cleanup())) try: loop.run_forever() diff --git a/examples/face_landmark/face_landmark.py b/examples/face_landmark/face_landmark.py index 4a209e74..649a5c6f 100644 --- a/examples/face_landmark/face_landmark.py +++ b/examples/face_landmark/face_landmark.py @@ -9,10 +9,9 @@ from mediapipe import solutions from mediapipe.framework.formats import landmark_pb2 -from livekit import rtc +from livekit import api, rtc -URL = "ws://localhost:7880" -TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5MDY2MTMyODgsImlzcyI6IkFQSVRzRWZpZFpqclFvWSIsIm5hbWUiOiJuYXRpdmUiLCJuYmYiOjE2NzI2MTMyODgsInN1YiI6Im5hdGl2ZSIsInZpZGVvIjp7InJvb20iOiJ0ZXN0Iiwicm9vbUFkbWluIjp0cnVlLCJyb29tQ3JlYXRlIjp0cnVlLCJyb29tSm9pbiI6dHJ1ZSwicm9vbUxpc3QiOnRydWV9fQ.uSNIangMRu8jZD5mnRYoCHjcsQWCrJXgHCs0aNIgBFY" # noqa +# ensure LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET are set tasks = set() @@ -30,10 +29,38 @@ running_mode=VisionRunningMode.VIDEO, ) -# from https://github.com/googlesamples/mediapipe/blob/main/examples/face_landmarker/python/%5BMediaPipe_Python_Tasks%5D_Face_Landmarker.ipynb + +async def main(room: rtc.Room) -> None: + video_stream = None + + @room.on("track_subscribed") + def on_track_subscribed(track: rtc.Track, *_): + if track.kind == rtc.TrackKind.KIND_VIDEO: + nonlocal video_stream + if video_stream is not None: + # only process the first stream received + return + + print("subscribed to track: " + track.name) + video_stream = rtc.VideoStream(track) + task = asyncio.create_task(frame_loop(video_stream)) + tasks.add(task) + task.add_done_callback(tasks.remove) + + info = api.ConnectionInfo() + token = api.AccessToken(info.api_key, info.api_secret) \ + .with_identity("python-bot") \ + .with_name("Python Bot") \ + .with_grants(api.VideoGrants( + room_join=True, + room="my-room", + )).to_jwt() + await room.connect(info.websocket_url(), token) + print("connected to room: " + room.name) def draw_landmarks_on_image(rgb_image, detection_result): + # from https://github.com/googlesamples/mediapipe/blob/main/examples/face_landmarker/python/%5BMediaPipe_Python_Tasks%5D_Face_Landmarker.ipynb face_landmarks_list = detection_result.face_landmarks # Loop through the detected faces to visualize. @@ -97,7 +124,8 @@ async def frame_loop(video_stream: rtc.VideoStream) -> None: mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=arr) - detection_result = landmarker.detect_for_video(mp_image, frame.timestamp_us) + detection_result = landmarker.detect_for_video( + mp_image, frame.timestamp_us) draw_landmarks_on_image(arr, detection_result) @@ -111,31 +139,11 @@ async def frame_loop(video_stream: rtc.VideoStream) -> None: cv2.destroyAllWindows() -async def main(room: rtc.Room) -> None: - video_stream = None - - @room.on("track_subscribed") - def on_track_subscribed(track: rtc.Track, *_): - if track.kind == rtc.TrackKind.KIND_VIDEO: - nonlocal video_stream - if video_stream is not None: - # only process the first stream received - return - - print("subscribed to track: " + track.name) - video_stream = rtc.VideoStream(track) - task = asyncio.create_task(frame_loop(video_stream)) - tasks.add(task) - task.add_done_callback(tasks.remove) - - await room.connect(URL, TOKEN) - print("connected to room: " + room.name) - - if __name__ == "__main__": logging.basicConfig( level=logging.INFO, - handlers=[logging.FileHandler("face_landmark.log"), logging.StreamHandler()], + handlers=[logging.FileHandler( + "face_landmark.log"), logging.StreamHandler()], ) loop = asyncio.get_event_loop() @@ -147,7 +155,8 @@ async def cleanup(): asyncio.ensure_future(main(room)) for signal in [SIGINT, SIGTERM]: - loop.add_signal_handler(signal, lambda: asyncio.ensure_future(cleanup())) + loop.add_signal_handler( + signal, lambda: asyncio.ensure_future(cleanup())) try: loop.run_forever() From 71aa008d3cf35f255cda3696f2d3df7494669d44 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Fri, 10 Nov 2023 21:47:07 -0800 Subject: [PATCH 3/7] remove connection_info --- README.md | 9 +- examples/basic_room.py | 32 ++++--- examples/face_landmark/face_landmark.py | 30 +++--- examples/publish_hue.py | 35 +++---- examples/publish_wave.py | 38 ++++---- examples/whisper/whisper.py | 105 ++++++++++++--------- livekit-api/livekit/api/__init__.py | 1 - livekit-api/livekit/api/access_token.py | 9 +- livekit-api/livekit/api/connection_info.py | 22 ----- 9 files changed, 140 insertions(+), 141 deletions(-) delete mode 100644 livekit-api/livekit/api/connection_info.py diff --git a/README.md b/README.md index 7ac60ce0..df3bb625 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,10 @@ from livekit import api import asyncio async def main(): - # loads connection info from environment variables - # LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET - info = api.ConnectionInfo() room_service = api.RoomService( - info.http_url(), - info.api_key, - info.api_secret, + 'http://localhost:7880', + 'devkey', + 'secret', ) room_info = await room_service.create_room( api.room.CreateRoomRequest(name="my-room"), diff --git a/examples/basic_room.py b/examples/basic_room.py index 7e898c30..f1bee7ea 100644 --- a/examples/basic_room.py +++ b/examples/basic_room.py @@ -2,6 +2,7 @@ import logging from signal import SIGINT, SIGTERM from typing import Union +import os from livekit import api, rtc @@ -104,8 +105,7 @@ def on_connection_quality_changed( def on_track_subscription_failed( participant: rtc.RemoteParticipant, track_sid: str, error: str ): - logging.info("track subscription failed: %s %s", - participant.identity, error) + logging.info("track subscription failed: %s %s", participant.identity, error) @room.on("connection_state_changed") def on_connection_state_changed(state: rtc.ConnectionState): @@ -127,15 +127,19 @@ def on_reconnecting() -> None: def on_reconnected() -> None: logging.info("reconnected") - info = api.ConnectionInfo() - token = api.AccessToken(info.api_key, info.api_secret) \ - .with_identity("python-bot") \ - .with_name("Python Bot") \ - .with_grants(api.VideoGrants( - room_join=True, - room="my-room", - )).to_jwt() - await room.connect(info.websocket_url(), token) + token = ( + api.AccessToken() + .with_identity("python-bot") + .with_name("Python Bot") + .with_grants( + api.VideoGrants( + room_join=True, + room="my-room", + ) + ) + .to_jwt() + ) + await room.connect(os.getenv("LIVEKIT_URL"), token) logging.info("connected to room %s", room.name) logging.info("participants: %s", room.participants) @@ -146,8 +150,7 @@ def on_reconnected() -> None: if __name__ == "__main__": logging.basicConfig( level=logging.INFO, - handlers=[logging.FileHandler( - "basic_room.log"), logging.StreamHandler()], + handlers=[logging.FileHandler("basic_room.log"), logging.StreamHandler()], ) loop = asyncio.get_event_loop() @@ -159,8 +162,7 @@ async def cleanup(): asyncio.ensure_future(main(room)) for signal in [SIGINT, SIGTERM]: - loop.add_signal_handler( - signal, lambda: asyncio.ensure_future(cleanup())) + loop.add_signal_handler(signal, lambda: asyncio.ensure_future(cleanup())) try: loop.run_forever() diff --git a/examples/face_landmark/face_landmark.py b/examples/face_landmark/face_landmark.py index 649a5c6f..72589897 100644 --- a/examples/face_landmark/face_landmark.py +++ b/examples/face_landmark/face_landmark.py @@ -47,15 +47,18 @@ def on_track_subscribed(track: rtc.Track, *_): tasks.add(task) task.add_done_callback(tasks.remove) - info = api.ConnectionInfo() - token = api.AccessToken(info.api_key, info.api_secret) \ - .with_identity("python-bot") \ - .with_name("Python Bot") \ - .with_grants(api.VideoGrants( - room_join=True, - room="my-room", - )).to_jwt() - await room.connect(info.websocket_url(), token) + token = ( + api.AccessToken() + .with_identity("python-bot") + .with_name("Python Bot") + .with_grants( + api.VideoGrants( + room_join=True, + room="my-room", + ) + ) + ) + await room.connect(os.getenv("LIVEKIT_URL"), token.to_jwt()) print("connected to room: " + room.name) @@ -124,8 +127,7 @@ async def frame_loop(video_stream: rtc.VideoStream) -> None: mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=arr) - detection_result = landmarker.detect_for_video( - mp_image, frame.timestamp_us) + detection_result = landmarker.detect_for_video(mp_image, frame.timestamp_us) draw_landmarks_on_image(arr, detection_result) @@ -142,8 +144,7 @@ async def frame_loop(video_stream: rtc.VideoStream) -> None: if __name__ == "__main__": logging.basicConfig( level=logging.INFO, - handlers=[logging.FileHandler( - "face_landmark.log"), logging.StreamHandler()], + handlers=[logging.FileHandler("face_landmark.log"), logging.StreamHandler()], ) loop = asyncio.get_event_loop() @@ -155,8 +156,7 @@ async def cleanup(): asyncio.ensure_future(main(room)) for signal in [SIGINT, SIGTERM]: - loop.add_signal_handler( - signal, lambda: asyncio.ensure_future(cleanup())) + loop.add_signal_handler(signal, lambda: asyncio.ensure_future(cleanup())) try: loop.run_forever() diff --git a/examples/publish_hue.py b/examples/publish_hue.py index a91ac741..3899c309 100644 --- a/examples/publish_hue.py +++ b/examples/publish_hue.py @@ -1,6 +1,7 @@ import asyncio import colorsys import logging +import os from signal import SIGINT, SIGTERM import numpy as np @@ -13,17 +14,22 @@ async def main(room: rtc.Room): - info = api.ConnectionInfo() - token = api.AccessToken(info.api_key, info.api_secret) \ - .with_identity("python-publisher") \ - .with_name("Python Publisher") \ - .with_grants(api.VideoGrants( - room_join=True, - room="my-room", - )).to_jwt() - logging.info("connecting to %s", info.websocket_url()) + token = ( + api.AccessToken() + .with_identity("python-publisher") + .with_name("Python Publisher") + .with_grants( + api.VideoGrants( + room_join=True, + room="my-room", + ) + ) + .to_jwt() + ) + url = os.getenv("LIVEKIT_URL") + logging.info("connecting to %s", url) try: - await room.connect(info.websocket_url(), token) + await room.connect(url, token) logging.info("connected to room %s", room.name) except rtc.ConnectError as e: logging.error("failed to connect to the room: %s", e) @@ -41,8 +47,7 @@ async def main(room: rtc.Room): async def draw_color_cycle(source: rtc.VideoSource): - argb_frame = rtc.ArgbFrame.create( - rtc.VideoFormatType.FORMAT_ARGB, WIDTH, HEIGHT) + argb_frame = rtc.ArgbFrame.create(rtc.VideoFormatType.FORMAT_ARGB, WIDTH, HEIGHT) arr = np.frombuffer(argb_frame.data, dtype=np.uint8) framerate = 1 / 30 @@ -74,8 +79,7 @@ async def draw_color_cycle(source: rtc.VideoSource): if __name__ == "__main__": logging.basicConfig( level=logging.INFO, - handlers=[logging.FileHandler( - "publish_hue.log"), logging.StreamHandler()], + handlers=[logging.FileHandler("publish_hue.log"), logging.StreamHandler()], ) loop = asyncio.get_event_loop() @@ -87,8 +91,7 @@ async def cleanup(): asyncio.ensure_future(main(room)) for signal in [SIGINT, SIGTERM]: - loop.add_signal_handler( - signal, lambda: asyncio.ensure_future(cleanup())) + loop.add_signal_handler(signal, lambda: asyncio.ensure_future(cleanup())) try: loop.run_forever() diff --git a/examples/publish_wave.py b/examples/publish_wave.py index 56b5c489..049601ba 100644 --- a/examples/publish_wave.py +++ b/examples/publish_wave.py @@ -1,6 +1,7 @@ import asyncio import logging from signal import SIGINT, SIGTERM +import os import numpy as np from livekit import rtc, api @@ -16,19 +17,24 @@ async def main(room: rtc.Room) -> None: def on_participant_disconnect(participant: rtc.Participant, *_): logging.info("participant disconnected: %s", participant.identity) - info = api.ConnectionInfo() - token = api.AccessToken(info.api_key, info.api_secret) \ - .with_identity("python-publisher") \ - .with_name("Python Publisher") \ - .with_grants(api.VideoGrants( - room_join=True, - room="my-room", - )).to_jwt() + token = ( + api.AccessToken() + .with_identity("python-publisher") + .with_name("Python Publisher") + .with_grants( + api.VideoGrants( + room_join=True, + room="my-room", + ) + ) + .to_jwt() + ) + url = os.getenv("LIVEKIT_URL") - logging.info("connecting to %s", info.websocket_url()) + logging.info("connecting to %s", url) try: await room.connect( - info.websocket_url(), + url, token, options=rtc.RoomOptions( auto_subscribe=True, @@ -55,13 +61,11 @@ async def publish_frames(source: rtc.AudioSource, frequency: int): samples_per_channel = 480 # 10ms at 48kHz time = np.arange(samples_per_channel) / SAMPLE_RATE total_samples = 0 - audio_frame = rtc.AudioFrame.create( - SAMPLE_RATE, NUM_CHANNELS, samples_per_channel) + audio_frame = rtc.AudioFrame.create(SAMPLE_RATE, NUM_CHANNELS, samples_per_channel) audio_data = np.frombuffer(audio_frame.data, dtype=np.int16) while True: time = (total_samples + np.arange(samples_per_channel)) / SAMPLE_RATE - sine_wave = (amplitude * np.sin(2 * np.pi * - frequency * time)).astype(np.int16) + sine_wave = (amplitude * np.sin(2 * np.pi * frequency * time)).astype(np.int16) np.copyto(audio_data, sine_wave) await source.capture_frame(audio_frame) total_samples += samples_per_channel @@ -70,8 +74,7 @@ async def publish_frames(source: rtc.AudioSource, frequency: int): if __name__ == "__main__": logging.basicConfig( level=logging.INFO, - handlers=[logging.FileHandler( - "publish_wave.log"), logging.StreamHandler()], + handlers=[logging.FileHandler("publish_wave.log"), logging.StreamHandler()], ) loop = asyncio.get_event_loop() @@ -83,8 +86,7 @@ async def cleanup(): asyncio.ensure_future(main(room)) for signal in [SIGINT, SIGTERM]: - loop.add_signal_handler( - signal, lambda: asyncio.ensure_future(cleanup())) + loop.add_signal_handler(signal, lambda: asyncio.ensure_future(cleanup())) try: loop.run_forever() diff --git a/examples/whisper/whisper.py b/examples/whisper/whisper.py index ced42b02..29669294 100644 --- a/examples/whisper/whisper.py +++ b/examples/whisper/whisper.py @@ -4,9 +4,10 @@ import pathlib import platform from signal import SIGINT, SIGTERM +import os import numpy as np -from livekit import rtc +from livekit import api, rtc os = platform.system().lower() if os == "windows": @@ -20,10 +21,6 @@ libname = str(whisper_dir / lib_file) fname_model = str(whisper_dir / "models/ggml-tiny.en.bin") -URL = "ws://localhost:7880" -TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5MDY2MTMyODgsImlzcyI6IkFQSVRzRWZpZFpqclFvWSIsIm5hbWUiOiJuYXRpdmUiLCJuYmYiOjE2NzI2MTMyODgsInN1YiI6Im5hdGl2ZSIsInZpZGVvIjp7InJvb20iOiJ0ZXN0Iiwicm9vbUFkbWluIjp0cnVlLCJyb29tQ3JlYXRlIjp0cnVlLCJyb29tSm9pbiI6dHJ1ZSwicm9vbUxpc3QiOnRydWV9fQ.uSNIangMRu8jZD5mnRYoCHjcsQWCrJXgHCs0aNIgBFY" # noqa - - # declare the Whisper C API (Only what we need, keep things simple) # also see this issue: https://github.com/ggerganov/whisper.cpp/issues/9 # structure must match https://github.com/ggerganov/whisper.cpp/blob/master/whisper.h @@ -98,6 +95,62 @@ class WhisperFullParams(ctypes.Structure): ctx = whisper.whisper_init_from_file(fname_model.encode("utf-8")) +async def main(room: rtc.Room): + @room.on("track_published") + def on_track_published( + publication: rtc.RemoteTrackPublication, participant: rtc.RemoteParticipant + ): + # Only subscribe to the audio tracks coming from the microphone + if ( + publication.kind == rtc.TrackKind.KIND_AUDIO + and publication.source == rtc.TrackSource.SOURCE_MICROPHONE + ): + logging.info( + "track published: %s from participant %s (%s), subscribing...", + publication.sid, + participant.sid, + participant.identity, + ) + + publication.set_subscribed(True) + + @room.on("track_subscribed") + def on_track_subscribed( + track: rtc.Track, + publication: rtc.RemoteTrackPublication, + participant: rtc.RemoteParticipant, + ): + logging.info("starting listening to: %s", participant.identity) + audio_stream = rtc.AudioStream(track) + asyncio.create_task(whisper_task(audio_stream)) + + url = os.getenv("LIVEKIT_URL") + token = ( + api.AccessToken() + .with_identity("python-bot") + .with_name("Python Bot") + .with_grants( + api.VideoGrants( + room_join=True, + room="my-room", + ) + ) + .to_jwt() + ) + # manually manage subscriptions + await room.connect(url, token, rtc.RoomOptions(auto_subscribe=False)) + logging.info("connected to room %s", room.name) + + # check if there are already published audio tracks + for participant in room.participants.values(): + for track in participant.tracks.values(): + if ( + track.kind == rtc.TrackKind.KIND_AUDIO + and track.source == rtc.TrackSource.SOURCE_MICROPHONE + ): + track.set_subscribed(True) + + async def whisper_task(stream: rtc.AudioStream): data_30_secs = np.zeros(SAMPLES_30_SECS, dtype=np.float32) written_samples = 0 # nb. of samples written to data_30_secs for the cur. inference @@ -150,48 +203,6 @@ async def whisper_task(stream: rtc.AudioStream): written_samples = 0 -async def main(room: rtc.Room): - @room.on("track_published") - def on_track_published( - publication: rtc.RemoteTrackPublication, participant: rtc.RemoteParticipant - ): - # Only subscribe to the audio tracks coming from the microphone - if ( - publication.kind == rtc.TrackKind.KIND_AUDIO - and publication.source == rtc.TrackSource.SOURCE_MICROPHONE - ): - logging.info( - "track published: %s from participant %s (%s), subscribing...", - publication.sid, - participant.sid, - participant.identity, - ) - - publication.set_subscribed(True) - - @room.on("track_subscribed") - def on_track_subscribed( - track: rtc.Track, - publication: rtc.RemoteTrackPublication, - participant: rtc.RemoteParticipant, - ): - logging.info("starting listening to: %s", participant.identity) - audio_stream = rtc.AudioStream(track) - asyncio.create_task(whisper_task(audio_stream)) - - await room.connect(URL, TOKEN, rtc.RoomOptions(auto_subscribe=False)) - logging.info("connected to room %s", room.name) - - # check if there are already published audio tracks - for participant in room.participants.values(): - for track in participant.tracks.values(): - if ( - track.kind == rtc.TrackKind.KIND_AUDIO - and track.source == rtc.TrackSource.SOURCE_MICROPHONE - ): - track.set_subscribed(True) - - if __name__ == "__main__": logging.basicConfig( level=logging.INFO, diff --git a/livekit-api/livekit/api/__init__.py b/livekit-api/livekit/api/__init__.py index 06de959d..e22f846d 100644 --- a/livekit-api/livekit/api/__init__.py +++ b/livekit-api/livekit/api/__init__.py @@ -22,7 +22,6 @@ from livekit.protocol import models from livekit.protocol import room -from .connection_info import ConnectionInfo from .access_token import VideoGrants, AccessToken from .room_service import RoomService from .version import __version__ diff --git a/livekit-api/livekit/api/access_token.py b/livekit-api/livekit/api/access_token.py index fe3df49a..7ce2029a 100644 --- a/livekit-api/livekit/api/access_token.py +++ b/livekit-api/livekit/api/access_token.py @@ -15,6 +15,7 @@ import calendar import dataclasses import datetime +import os import jwt @@ -69,10 +70,16 @@ class Claims: class AccessToken: - def __init__(self, api_key: str, api_secret: str) -> None: + def __init__( + self, + api_key: str = os.getenv("LIVEKIT_API_KEY"), + api_secret: str = os.getenv("LIVEKIT_API_SECRET"), + ) -> None: self.api_key = api_key # iss self.api_secret = api_secret self.claims = Claims() + if api_key is None or api_secret is None: + raise ValueError("api_key and api_secret must be set") # default jwt claims self.identity = "" # sub diff --git a/livekit-api/livekit/api/connection_info.py b/livekit-api/livekit/api/connection_info.py deleted file mode 100644 index 7e5cca70..00000000 --- a/livekit-api/livekit/api/connection_info.py +++ /dev/null @@ -1,22 +0,0 @@ -import os - - -class ConnectionInfo: - def __init__(self): - self.url = os.getenv("LIVEKIT_URL", "ws://localhost:7880") - self.api_key = os.getenv("LIVEKIT_API_KEY") - self.api_secret = os.getenv("LIVEKIT_API_SECRET") - - if not self.api_key or not self.api_secret: - raise ValueError( - "LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set") - - def websocket_url(self) -> str: - if self.url.startswith("http"): - return self.url.replace("http", "ws", 1) - return self.url - - def http_url(self) -> str: - if self.url.startswith("ws"): - return self.url.replace("ws", "http", 1) - return self.url From 374c7003976baa406cac521e269c65b48853a540 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Fri, 10 Nov 2023 21:49:04 -0800 Subject: [PATCH 4/7] fix --- examples/whisper/whisper.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/whisper/whisper.py b/examples/whisper/whisper.py index 29669294..1b883c65 100644 --- a/examples/whisper/whisper.py +++ b/examples/whisper/whisper.py @@ -4,15 +4,15 @@ import pathlib import platform from signal import SIGINT, SIGTERM -import os +from os import getenv import numpy as np from livekit import api, rtc -os = platform.system().lower() -if os == "windows": +platform = platform.system().lower() +if platform == "windows": lib_file = "whisper.dll" -elif os == "darwin": +elif platform == "darwin": lib_file = "libwhisper.dylib" else: lib_file = "libwhisper.so" @@ -124,7 +124,7 @@ def on_track_subscribed( audio_stream = rtc.AudioStream(track) asyncio.create_task(whisper_task(audio_stream)) - url = os.getenv("LIVEKIT_URL") + url = getenv("LIVEKIT_URL") token = ( api.AccessToken() .with_identity("python-bot") From ce7761e09ac2b74c2c4770e058866b059d3ee665 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Fri, 10 Nov 2023 21:52:57 -0800 Subject: [PATCH 5/7] fix readme reference --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index df3bb625..24ef0a9f 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,9 @@ $ pip install livekit-api ```python from livekit import api +import os -token = api.AccessToken(info.api_key, info.api_secret) \ +token = api.AccessToken(os.getenv('LIVEKIT_API_KEY'), os.getenv('LIVEKIT_API_SECRET')) \ .with_identity("python-bot") \ .with_name("Python Bot") \ .with_grants(api.VideoGrants( From 7c5949bee3dd64191332d075bbc3ae4791ff927a Mon Sep 17 00:00:00 2001 From: David Zhao Date: Fri, 10 Nov 2023 21:53:42 -0800 Subject: [PATCH 6/7] Update livekit-api/livekit/api/access_token.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ThΓ©o Monnom --- livekit-api/livekit/api/access_token.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/livekit-api/livekit/api/access_token.py b/livekit-api/livekit/api/access_token.py index 7ce2029a..2d30bad8 100644 --- a/livekit-api/livekit/api/access_token.py +++ b/livekit-api/livekit/api/access_token.py @@ -72,13 +72,13 @@ class Claims: class AccessToken: def __init__( self, - api_key: str = os.getenv("LIVEKIT_API_KEY"), - api_secret: str = os.getenv("LIVEKIT_API_SECRET"), + api_key: str = os.getenv("LIVEKIT_API_KEY", ""), + api_secret: str = os.getenv("LIVEKIT_API_SECRET", ""), ) -> None: self.api_key = api_key # iss self.api_secret = api_secret self.claims = Claims() - if api_key is None or api_secret is None: + if not api_key or not api_secret : raise ValueError("api_key and api_secret must be set") # default jwt claims From f20f868e862027c04953d606049b8f7317cb2847 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Fri, 10 Nov 2023 21:58:33 -0800 Subject: [PATCH 7/7] last one --- livekit-api/livekit/api/access_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit-api/livekit/api/access_token.py b/livekit-api/livekit/api/access_token.py index 2d30bad8..87fecd88 100644 --- a/livekit-api/livekit/api/access_token.py +++ b/livekit-api/livekit/api/access_token.py @@ -78,7 +78,7 @@ def __init__( self.api_key = api_key # iss self.api_secret = api_secret self.claims = Claims() - if not api_key or not api_secret : + if not api_key or not api_secret: raise ValueError("api_key and api_secret must be set") # default jwt claims