Skip to content

Commit e00dcc8

Browse files
committed
init media devices
1 parent c8ec56d commit e00dcc8

File tree

4 files changed

+622
-0
lines changed

4 files changed

+622
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import os
2+
import asyncio
3+
import logging
4+
5+
from livekit import rtc
6+
7+
8+
async def main() -> None:
9+
logging.basicConfig(level=logging.INFO)
10+
11+
url = os.getenv("LIVEKIT_URL")
12+
token = os.getenv("LIVEKIT_TOKEN")
13+
if not url or not token:
14+
raise RuntimeError("LIVEKIT_URL and LIVEKIT_TOKEN must be set in env")
15+
16+
room = rtc.Room()
17+
devices = rtc.MediaDevices()
18+
19+
# Open microphone with AEC and prepare a player for remote audio feeding AEC reverse stream
20+
mic = devices.open_microphone(enable_aec=True)
21+
player = devices.open_output_player(apm_for_reverse=mic.apm)
22+
23+
# Mixer for all remote audio streams
24+
mixer = rtc.AudioMixer(sample_rate=48000, num_channels=1)
25+
26+
# Track stream bookkeeping for cleanup
27+
streams_by_pub: dict[str, rtc.AudioStream] = {}
28+
streams_by_participant: dict[str, set[rtc.AudioStream]] = {}
29+
30+
async def _remove_stream(stream: rtc.AudioStream, participant_sid: str | None = None, pub_sid: str | None = None) -> None:
31+
try:
32+
mixer.remove_stream(stream)
33+
except Exception:
34+
pass
35+
try:
36+
await stream.aclose()
37+
except Exception:
38+
pass
39+
if participant_sid and participant_sid in streams_by_participant:
40+
streams_by_participant.get(participant_sid, set()).discard(stream)
41+
if not streams_by_participant.get(participant_sid):
42+
streams_by_participant.pop(participant_sid, None)
43+
if pub_sid is not None:
44+
streams_by_pub.pop(pub_sid, None)
45+
46+
async def on_track_subscribed(track: rtc.Track, publication: rtc.RemoteTrackPublication, participant: rtc.RemoteParticipant):
47+
if track.kind == rtc.TrackKind.KIND_AUDIO:
48+
stream = rtc.AudioStream(track, sample_rate=48000, num_channels=1)
49+
streams_by_pub[publication.sid] = stream
50+
streams_by_participant.setdefault(participant.sid, set()).add(stream)
51+
mixer.add_stream(stream)
52+
logging.info("subscribed to audio from %s", participant.identity)
53+
54+
room.on("track_subscribed", on_track_subscribed)
55+
56+
def on_track_unsubscribed(track: rtc.Track, publication: rtc.RemoteTrackPublication, participant: rtc.RemoteParticipant):
57+
stream = streams_by_pub.get(publication.sid)
58+
if stream is not None:
59+
asyncio.create_task(_remove_stream(stream, participant.sid, publication.sid))
60+
logging.info("unsubscribed from audio of %s", participant.identity)
61+
62+
room.on("track_unsubscribed", on_track_unsubscribed)
63+
64+
def on_track_unpublished(publication: rtc.RemoteTrackPublication, participant: rtc.RemoteParticipant):
65+
stream = streams_by_pub.get(publication.sid)
66+
if stream is not None:
67+
asyncio.create_task(_remove_stream(stream, participant.sid, publication.sid))
68+
logging.info("track unpublished: %s from %s", publication.sid, participant.identity)
69+
70+
room.on("track_unpublished", on_track_unpublished)
71+
72+
def on_participant_disconnected(participant: rtc.RemoteParticipant):
73+
streams = list(streams_by_participant.pop(participant.sid, set()))
74+
for stream in streams:
75+
# Best-effort discover publication sid
76+
pub_sid = None
77+
for k, v in list(streams_by_pub.items()):
78+
if v is stream:
79+
pub_sid = k
80+
break
81+
asyncio.create_task(_remove_stream(stream, participant.sid, pub_sid))
82+
logging.info("participant disconnected: %s", participant.identity)
83+
84+
room.on("participant_disconnected", on_participant_disconnected)
85+
86+
try:
87+
await room.connect(url, token)
88+
logging.info("connected to room %s", room.name)
89+
90+
# Publish microphone
91+
track = rtc.LocalAudioTrack.create_audio_track("mic", mic.source)
92+
pub_opts = rtc.TrackPublishOptions()
93+
pub_opts.source = rtc.TrackSource.SOURCE_MICROPHONE
94+
await room.local_participant.publish_track(track, pub_opts)
95+
logging.info("published local microphone")
96+
97+
# Start playing mixed remote audio
98+
play_task = asyncio.create_task(player.play(mixer))
99+
100+
# Run until Ctrl+C
101+
while True:
102+
await asyncio.sleep(1)
103+
except KeyboardInterrupt:
104+
pass
105+
finally:
106+
await mic.aclose()
107+
await mixer.aclose()
108+
await player.aclose()
109+
try:
110+
await room.disconnect()
111+
except Exception:
112+
pass
113+
114+
115+
if __name__ == "__main__":
116+
asyncio.run(main())
117+
118+
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import os
2+
import sys
3+
import asyncio
4+
import logging
5+
6+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "livekit-rtc")))
7+
8+
from livekit import rtc
9+
from livekit.rtc import MediaDevices
10+
11+
async def main() -> None:
12+
logging.basicConfig(level=logging.INFO)
13+
14+
url = os.getenv("LIVEKIT_URL")
15+
token = os.getenv("LIVEKIT_TOKEN")
16+
if not url or not token:
17+
raise RuntimeError("LIVEKIT_URL and LIVEKIT_TOKEN must be set in env")
18+
19+
room = rtc.Room()
20+
21+
# Create media devices helper and open default microphone with AEC enabled
22+
devices = MediaDevices()
23+
mic = devices.open_microphone(enable_aec=True)
24+
25+
try:
26+
await room.connect(url, token)
27+
logging.info("connected to room %s", room.name)
28+
29+
track = rtc.LocalAudioTrack.create_audio_track("mic", mic.source)
30+
pub_opts = rtc.TrackPublishOptions()
31+
pub_opts.source = rtc.TrackSource.SOURCE_MICROPHONE
32+
await room.local_participant.publish_track(track, pub_opts)
33+
logging.info("published local microphone")
34+
35+
# Run until Ctrl+C
36+
while True:
37+
await asyncio.sleep(1)
38+
except KeyboardInterrupt:
39+
pass
40+
finally:
41+
await mic.aclose()
42+
try:
43+
await room.disconnect()
44+
except Exception:
45+
pass
46+
47+
48+
if __name__ == "__main__":
49+
asyncio.run(main())
50+
51+

livekit-rtc/livekit/rtc/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@
9090
from .audio_resampler import AudioResampler, AudioResamplerQuality
9191
from .audio_mixer import AudioMixer
9292
from .apm import AudioProcessingModule
93+
try:
94+
from .media_devices import MediaDevices
95+
_HAS_MEDIA_DEVICES = True
96+
except Exception: # pragma: no cover - optional dependency (sounddevice)
97+
_HAS_MEDIA_DEVICES = False
9398
from .utils import combine_audio_frames
9499
from .rpc import RpcError, RpcInvocationData
95100
from .synchronizer import AVSynchronizer

0 commit comments

Comments
 (0)