Skip to content

Commit 8ddc123

Browse files
committed
In the warm transfer example, add hold music
1 parent e590aeb commit 8ddc123

File tree

4 files changed

+179
-25
lines changed

4 files changed

+179
-25
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Hold Music Player
2+
3+
This project is a hold music player, based on the `wav_audio_send` example from the [daily-python repository](https://github.com/daily-co/daily-python/blob/main/demos/audio/wav_audio_send.py).
4+
5+
The hold music WAV file used in this example was sourced from [No Copyright Music](https://www.no-copyright-music.com/).
6+
7+
To see this hold music player in use, check out the [warm transfer example](../warm_transfer.py).
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#
2+
# This demo will join a Daily meeting and send the audio from a WAV file into
3+
# the meeting.
4+
#
5+
# Usage: python3 wav_audio_send.py -m MEETING_URL -i FILE.wav
6+
#
7+
8+
import argparse
9+
import threading
10+
import wave
11+
12+
from daily import *
13+
14+
SAMPLE_RATE = 16000
15+
NUM_CHANNELS = 1
16+
17+
18+
class SendWavApp:
19+
def __init__(self, input_file_name, sample_rate, num_channels):
20+
self.__mic_device = Daily.create_microphone_device(
21+
"my-mic", sample_rate=sample_rate, channels=num_channels
22+
)
23+
24+
self.__client = CallClient()
25+
26+
self.__client.update_subscription_profiles(
27+
{"base": {"camera": "unsubscribed", "microphone": "unsubscribed"}}
28+
)
29+
30+
self.__app_quit = False
31+
self.__app_error = None
32+
33+
self.__start_event = threading.Event()
34+
self.__thread = threading.Thread(target=self.send_wav_file, args=[input_file_name])
35+
self.__thread.start()
36+
37+
def on_joined(self, data, error):
38+
if error:
39+
print(f"Unable to join meeting: {error}")
40+
self.__app_error = error
41+
self.__start_event.set()
42+
43+
def run(self, meeting_url, meeting_token):
44+
self.__client.join(
45+
meeting_url,
46+
meeting_token,
47+
client_settings={
48+
"inputs": {
49+
"camera": False,
50+
"microphone": {"isEnabled": True, "settings": {"deviceId": "my-mic"}},
51+
}
52+
},
53+
completion=self.on_joined,
54+
)
55+
self.__thread.join()
56+
57+
def leave(self):
58+
self.__app_quit = True
59+
self.__thread.join()
60+
self.__client.leave()
61+
self.__client.release()
62+
63+
def send_wav_file(self, file_name):
64+
self.__start_event.wait()
65+
66+
if self.__app_error:
67+
print(f"Unable to send WAV file!")
68+
return
69+
70+
wav = wave.open(file_name, "rb")
71+
72+
sent_frames = 0
73+
total_frames = wav.getnframes()
74+
sample_rate = wav.getframerate()
75+
while not self.__app_quit and sent_frames < total_frames:
76+
# Read 100ms worth of audio frames.
77+
frames = wav.readframes(int(sample_rate / 10))
78+
if len(frames) > 0:
79+
self.__mic_device.write_frames(frames)
80+
sent_frames += sample_rate / 10
81+
82+
83+
def main():
84+
parser = argparse.ArgumentParser()
85+
parser.add_argument("-m", "--meeting", required=True, help="Meeting URL")
86+
parser.add_argument("-t", "--token", required=True, help="Meeting token")
87+
parser.add_argument("-i", "--input", required=True, help="WAV input file")
88+
parser.add_argument(
89+
"-c", "--channels", type=int, default=NUM_CHANNELS, help="Number of channels"
90+
)
91+
parser.add_argument("-r", "--rate", type=int, default=SAMPLE_RATE, help="Sample rate")
92+
93+
args = parser.parse_args()
94+
95+
Daily.init()
96+
97+
app = SendWavApp(args.input, args.rate, args.channels)
98+
99+
try:
100+
app.run(args.meeting, args.token)
101+
except KeyboardInterrupt:
102+
print("Ctrl-C detected. Exiting!")
103+
finally:
104+
app.leave()
105+
106+
107+
if __name__ == "__main__":
108+
main()
4.74 MB
Binary file not shown.

examples/dynamic/warm_transfer.py

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import sys
4141
from pathlib import Path
4242
from typing import Any, Dict
43+
import atexit
4344

4445
import aiohttp
4546
from dotenv import load_dotenv
@@ -155,6 +156,20 @@ async def mute_customer(action: dict, flow_manager: FlowManager):
155156
)
156157

157158

159+
async def start_hold_music(action: dict, flow_manager: FlowManager):
160+
hold_music_args = flow_manager.state["hold_music_args"]
161+
flow_manager.state["hold_music_process"] = await asyncio.create_subprocess_exec(
162+
sys.executable,
163+
str(hold_music_args["script_path"]),
164+
"-m",
165+
hold_music_args["room_url"],
166+
"-t",
167+
hold_music_args["token"],
168+
"-i",
169+
hold_music_args["wav_file_path"],
170+
)
171+
172+
158173
async def make_customer_hear_only_hold_music(action: dict, flow_manager: FlowManager):
159174
"""Make it so the customer only hears hold music.
160175
@@ -167,15 +182,7 @@ async def make_customer_hear_only_hold_music(action: dict, flow_manager: FlowMan
167182
await transport.update_remote_participants(
168183
remote_participants={
169184
customer_participant_id: {
170-
"permissions": {
171-
"canReceive": {
172-
"byUserId": {
173-
"bot": {
174-
"customAudio": {"hold-music": True},
175-
}
176-
}
177-
}
178-
}
185+
"permissions": {"canReceive": {"byUserId": {"hold-music": True}}}
179186
}
180187
}
181188
)
@@ -373,6 +380,7 @@ def create_transferring_to_human_agent_node() -> NodeConfig:
373380
ActionConfig(type="function", handler=mute_customer),
374381
],
375382
post_actions=[
383+
ActionConfig(type="function", handler=start_hold_music),
376384
ActionConfig(type="function", handler=make_customer_hear_only_hold_music),
377385
ActionConfig(type="function", handler=print_human_agent_join_url),
378386
],
@@ -472,7 +480,6 @@ def get_human_agent_participant_id(transport: DailyTransport) -> str:
472480

473481
async def get_customer_token(daily_rest_helper: DailyRESTHelper, room_url: str) -> str:
474482
"""Gets a Daily token for the customer, configured with properties:
475-
476483
{
477484
user_id: "customer",
478485
permissions: {
@@ -484,50 +491,61 @@ async def get_customer_token(daily_rest_helper: DailyRESTHelper, room_url: str)
484491
}
485492
}
486493
}
487-
488-
Note that they'll join only being able to hear the bot.
489494
"""
490495
return await get_token(
491496
user_id="customer",
492-
permissions={"canReceive": {"base": False, "byUserId": {"bot": True}}},
497+
permissions={
498+
"canReceive": {
499+
"base": False,
500+
"byUserId": {
501+
"bot": True,
502+
},
503+
}
504+
},
493505
daily_rest_helper=daily_rest_helper,
494506
room_url=room_url,
495507
)
496508

497509

498510
async def get_human_agent_token(daily_rest_helper: DailyRESTHelper, room_url: str) -> str:
499511
"""Gets a Daily token for the human agent, configured with properties:
500-
501512
{
502513
user_id: "agent",
503514
permissions: {
504515
canReceive: {
505516
base: false,
506517
byUserId: {
507-
bot: {
508-
audio: true,
509-
customAudio: { "hold-music": false }
510-
}
518+
bot: true
511519
}
512520
}
513521
}
514522
}
515-
516-
Note that they'll join only being able to hear the bot's audio (and not the hold music).
517523
"""
518524
return await get_token(
519525
user_id="agent",
520526
permissions={
521527
"canReceive": {
522528
"base": False,
523-
"byUserId": {"bot": {"audio": True, "customAudio": {"hold-music": False}}},
529+
"byUserId": {
530+
"bot": True,
531+
},
524532
}
525533
},
526534
daily_rest_helper=daily_rest_helper,
527535
room_url=room_url,
528536
)
529537

530538

539+
async def get_hold_music_player_token(daily_rest_helper: DailyRESTHelper, room_url: str) -> str:
540+
"""Gets a Daily token for the hold music player"""
541+
return await get_token(
542+
user_id="hold-music",
543+
permissions={},
544+
daily_rest_helper=daily_rest_helper,
545+
room_url=room_url,
546+
)
547+
548+
531549
async def get_token(
532550
user_id: str, permissions: dict, daily_rest_helper: DailyRESTHelper, room_url: str
533551
) -> str:
@@ -622,11 +640,11 @@ async def on_participant_left(
622640
):
623641
# NOTE: an opportunity for refinement here is to handle the customer leaving while on
624642
# hold, informing the human agent if needed
625-
"""If all non-bot participants have left, stop the bot"""
626-
non_bot_participants = {
627-
k: v for k, v in transport.participants().items() if not v["info"]["isLocal"]
643+
"""If all human participants have left, stop the bot"""
644+
human_participants = {
645+
k: v for k, v in transport.participants().items() if v.get("info", {}).get("userId") in {"agent", "customer"}
628646
}
629-
if not non_bot_participants:
647+
if not human_participants:
630648
await task.cancel()
631649

632650
# Print URL for joining as customer, and store URL for joining as human agent, to be printed later
@@ -649,6 +667,27 @@ async def on_participant_left(
649667
f"{room_url}{'?' if '?' not in room_url else '&'}t={human_agent_token}"
650668
)
651669

670+
# Prepare hold music args
671+
flow_manager.state["hold_music_args"] = {
672+
"script_path": Path(__file__).parent / "hold_music" / "hold_music.py",
673+
"wav_file_path": Path(__file__).parent / "hold_music" / "hold_music.wav",
674+
"room_url": room_url,
675+
"token": await get_hold_music_player_token(
676+
daily_rest_helper=daily_rest_helper, room_url=room_url
677+
),
678+
}
679+
680+
# Clean up hold music process at exit, if needed
681+
def cleanup_hold_music_process():
682+
hold_music_process = flow_manager.state.get("hold_music_process")
683+
if hold_music_process:
684+
try:
685+
hold_music_process.terminate()
686+
except:
687+
# Exception if process already done; we don't care, it didn't hurt to try
688+
pass
689+
atexit.register(cleanup_hold_music_process)
690+
652691
# Run the pipeline
653692
runner = PipelineRunner()
654693
await runner.run(task)

0 commit comments

Comments
 (0)