DailyTransport: replace virtual microphone with custom microphone track
This commit is contained in:
@@ -76,6 +76,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- `DailyTransport` now uses custom microphone audio tracks instead of virtual
|
||||||
|
microphones. Now, multiple Daily transports can be used in the same process.
|
||||||
|
|
||||||
- `DailyTransport` now captures audio from individual participants instead of
|
- `DailyTransport` now captures audio from individual participants instead of
|
||||||
the whole room. This allows identifying audio frames per participant.
|
the whole room. This allows identifying audio frames per participant.
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ async def main():
|
|||||||
token,
|
token,
|
||||||
"Respond bot",
|
"Respond bot",
|
||||||
DailyParams(
|
DailyParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
transcription_enabled=True,
|
transcription_enabled=True,
|
||||||
vad_enabled=True,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -44,11 +44,11 @@ try:
|
|||||||
AudioData,
|
AudioData,
|
||||||
CallClient,
|
CallClient,
|
||||||
CustomAudioSource,
|
CustomAudioSource,
|
||||||
|
CustomAudioTrack,
|
||||||
Daily,
|
Daily,
|
||||||
EventHandler,
|
EventHandler,
|
||||||
VideoFrame,
|
VideoFrame,
|
||||||
VirtualCameraDevice,
|
VirtualCameraDevice,
|
||||||
VirtualMicrophoneDevice,
|
|
||||||
)
|
)
|
||||||
except ModuleNotFoundError as e:
|
except ModuleNotFoundError as e:
|
||||||
logger.error(f"Exception: {e}")
|
logger.error(f"Exception: {e}")
|
||||||
@@ -242,6 +242,12 @@ def completion_callback(future):
|
|||||||
return _callback
|
return _callback
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DailyAudioTrack:
|
||||||
|
source: CustomAudioSource
|
||||||
|
track: CustomAudioTrack
|
||||||
|
|
||||||
|
|
||||||
class DailyTransportClient(EventHandler):
|
class DailyTransportClient(EventHandler):
|
||||||
"""Core client for interacting with Daily's API.
|
"""Core client for interacting with Daily's API.
|
||||||
|
|
||||||
@@ -319,15 +325,12 @@ class DailyTransportClient(EventHandler):
|
|||||||
self._out_sample_rate = 0
|
self._out_sample_rate = 0
|
||||||
|
|
||||||
self._camera: Optional[VirtualCameraDevice] = None
|
self._camera: Optional[VirtualCameraDevice] = None
|
||||||
self._mic: Optional[VirtualMicrophoneDevice] = None
|
self._microphone_track: Optional[DailyAudioTrack] = None
|
||||||
self._audio_sources: Dict[str, CustomAudioSource] = {}
|
self._custom_audio_tracks: Dict[str, DailyAudioTrack] = {}
|
||||||
|
|
||||||
def _camera_name(self):
|
def _camera_name(self):
|
||||||
return f"camera-{self}"
|
return f"camera-{self}"
|
||||||
|
|
||||||
def _mic_name(self):
|
|
||||||
return f"mic-{self}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def room_url(self) -> str:
|
def room_url(self) -> str:
|
||||||
return self._room_url
|
return self._room_url
|
||||||
@@ -359,19 +362,25 @@ class DailyTransportClient(EventHandler):
|
|||||||
await future
|
await future
|
||||||
|
|
||||||
async def register_audio_destination(self, destination: str):
|
async def register_audio_destination(self, destination: str):
|
||||||
self._audio_sources[destination] = await self.add_custom_audio_track(destination)
|
self._custom_audio_tracks[destination] = await self.add_custom_audio_track(destination)
|
||||||
self._client.update_publishing({"customAudio": {destination: True}})
|
self._client.update_publishing({"customAudio": {destination: True}})
|
||||||
|
|
||||||
async def write_raw_audio_frames(self, frames: bytes, destination: Optional[str] = None):
|
async def write_raw_audio_frames(self, frames: bytes, destination: Optional[str] = None):
|
||||||
future = self._get_event_loop().create_future()
|
future = self._get_event_loop().create_future()
|
||||||
if not destination and self._mic:
|
|
||||||
self._mic.write_frames(frames, completion=completion_callback(future))
|
audio_source: Optional[CustomAudioSource] = None
|
||||||
elif destination and destination in self._audio_sources:
|
if not destination and self._microphone_track:
|
||||||
source = self._audio_sources[destination]
|
audio_source = self._microphone_track.source
|
||||||
source.write_frames(frames, completion=completion_callback(future))
|
elif destination and destination in self._custom_audio_tracks:
|
||||||
|
track = self._custom_audio_tracks[destination]
|
||||||
|
audio_source = track.source
|
||||||
|
|
||||||
|
if audio_source:
|
||||||
|
audio_source.write_frames(frames, completion=completion_callback(future))
|
||||||
else:
|
else:
|
||||||
logger.warning(f"{self} unable to write audio frames to destination [{destination}]")
|
logger.warning(f"{self} unable to write audio frames to destination [{destination}]")
|
||||||
future.set_result(None)
|
future.set_result(None)
|
||||||
|
|
||||||
await future
|
await future
|
||||||
|
|
||||||
async def write_raw_video_frame(
|
async def write_raw_video_frame(
|
||||||
@@ -410,13 +419,10 @@ class DailyTransportClient(EventHandler):
|
|||||||
color_format=self._params.video_out_color_format,
|
color_format=self._params.video_out_color_format,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._params.audio_out_enabled and not self._mic:
|
if self._params.audio_out_enabled and not self._microphone_track:
|
||||||
self._mic = Daily.create_microphone_device(
|
audio_source = CustomAudioSource(self._out_sample_rate, self._params.audio_out_channels)
|
||||||
self._mic_name(),
|
audio_track = CustomAudioTrack(audio_source)
|
||||||
sample_rate=self._out_sample_rate,
|
self._microphone_track = DailyAudioTrack(source=audio_source, track=audio_track)
|
||||||
channels=self._params.audio_out_channels,
|
|
||||||
non_blocking=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def join(self):
|
async def join(self):
|
||||||
# Transport already joined or joining, ignore.
|
# Transport already joined or joining, ignore.
|
||||||
@@ -501,12 +507,11 @@ class DailyTransportClient(EventHandler):
|
|||||||
"microphone": {
|
"microphone": {
|
||||||
"isEnabled": microphone_enabled,
|
"isEnabled": microphone_enabled,
|
||||||
"settings": {
|
"settings": {
|
||||||
"deviceId": self._mic_name(),
|
"customTrack": {
|
||||||
"customConstraints": {
|
"id": self._microphone_track.track.id
|
||||||
"autoGainControl": {"exact": False},
|
if self._microphone_track
|
||||||
"echoCancellation": {"exact": False},
|
else "no-microphone-track"
|
||||||
"noiseSuppression": {"exact": False},
|
}
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -553,7 +558,7 @@ class DailyTransportClient(EventHandler):
|
|||||||
await self._stop_transcription()
|
await self._stop_transcription()
|
||||||
|
|
||||||
# Remove any custom tracks, if any.
|
# Remove any custom tracks, if any.
|
||||||
for track_name, _ in self._audio_sources.items():
|
for track_name, _ in self._custom_audio_tracks.items():
|
||||||
await self.remove_custom_audio_track(track_name)
|
await self.remove_custom_audio_track(track_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -703,19 +708,24 @@ class DailyTransportClient(EventHandler):
|
|||||||
color_format=color_format,
|
color_format=color_format,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def add_custom_audio_track(self, track_name: str) -> CustomAudioSource:
|
async def add_custom_audio_track(self, track_name: str) -> DailyAudioTrack:
|
||||||
future = self._get_event_loop().create_future()
|
future = self._get_event_loop().create_future()
|
||||||
|
|
||||||
audio_source = CustomAudioSource(self._out_sample_rate, 1)
|
audio_source = CustomAudioSource(self._out_sample_rate, 1)
|
||||||
|
|
||||||
|
audio_track = CustomAudioTrack(audio_source)
|
||||||
|
|
||||||
self._client.add_custom_audio_track(
|
self._client.add_custom_audio_track(
|
||||||
track_name=track_name,
|
track_name=track_name,
|
||||||
audio_source=audio_source,
|
audio_track=audio_track,
|
||||||
completion=completion_callback(future),
|
completion=completion_callback(future),
|
||||||
)
|
)
|
||||||
|
|
||||||
await future
|
await future
|
||||||
|
|
||||||
return audio_source
|
track = DailyAudioTrack(source=audio_source, track=audio_track)
|
||||||
|
|
||||||
|
return track
|
||||||
|
|
||||||
async def remove_custom_audio_track(self, track_name: str):
|
async def remove_custom_audio_track(self, track_name: str):
|
||||||
future = self._get_event_loop().create_future()
|
future = self._get_event_loop().create_future()
|
||||||
|
|||||||
Reference in New Issue
Block a user