Skip to content

Commit 6376e16

Browse files
jamseaclaude
andcommitted
Migrate bot-ready-signalling example to Pipecat client SDK
Replace raw @daily-co/daily-js plumbing with @pipecat-ai/client-js plus @pipecat-ai/daily-transport (web) and @pipecat-ai/react-native-daily-transport (RN). The custom sendAppMessage("playable") workaround is removed in favour of the standard RTVI client-ready / bot-ready handshake: RTVIProcessor is auto-attached to PipelineTask, and the bot pushes the first TTSSpeakFrame from @task.rtvi.event_handler("on_client_ready"). - Server: /connect now returns {url, token} (the shape DailyTransport expects). signalling_bot.py drops the SilenceFrame workaround and on_app_message handler, adds transport.input(), and queues the greeting from on_client_ready. - JS client: rewritten around PipecatClient + DailyTransport with startBotAndConnect; bot audio is rendered via the onTrackStarted callback. - RN client: rewritten around PipecatClient + RNDailyTransport. Expo bumped from 52 to 54 and RN to 0.81 to satisfy the transport's webrtc peer dep (matches pipecat-examples/simple-chatbot). - READMEs updated to describe the RTVI handshake. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ae5d03f commit 6376e16

11 files changed

Lines changed: 266 additions & 291 deletions

File tree

bot-ready-signalling/README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
# Bot ready signaling
22

3-
A simple Pipecat example demonstrating how to handle signaling between the client and the bot,
4-
ensuring that the bot starts sending audio only when the client is available,
5-
thereby avoiding the risk of cutting off the beginning of the audio.
3+
A simple Pipecat example demonstrating how to handle signaling between the
4+
client and the bot, ensuring that the bot starts sending audio only after the
5+
client is ready to play it, so the first words of the greeting are never
6+
clipped.
7+
8+
The handshake uses the standard RTVI `client-ready` / `bot-ready` flow that
9+
ships with Pipecat: `RTVIProcessor` is auto-attached to every `PipelineTask`,
10+
the Pipecat client SDK signals `client-ready` once the transport reaches the
11+
`ready` state, and the bot's `on_client_ready` handler calls `set_bot_ready()`
12+
and pushes the first `TTSSpeakFrame`. No custom `sendAppMessage` plumbing is
13+
needed.
614

715
## Quick Start
816

bot-ready-signalling/client/javascript/README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# JavaScript Implementation
22

3-
Basic implementation using the [Pipecat JavaScript SDK](https://docs.pipecat.ai/client/js/introduction).
3+
Basic implementation using the [Pipecat JavaScript SDK](https://docs.pipecat.ai/client/js/introduction)
4+
with the [Daily transport](https://docs.pipecat.ai/api-reference/client/js/transports).
45

56
## Setup
67

7-
1. Run the bot server. See the [server README](../../README).
8+
1. Run the bot server. See the [top-level README](../../README.md).
89

910
2. Navigate to the `client/javascript` directory:
1011

@@ -24,4 +25,15 @@ npm install
2425
npm run dev
2526
```
2627

27-
5. Visit http://localhost:5173 in your browser.
28+
5. Visit http://localhost:5173 in your browser, then click **Connect**.
29+
30+
## How the bot-ready handshake works
31+
32+
The Pipecat JavaScript client signals `client-ready` automatically once the
33+
transport reaches the `ready` state. The bot's `on_client_ready` handler then
34+
calls `set_bot_ready()` and pushes the first `TTSSpeakFrame`, so the greeting
35+
is never clipped. The previous `sendAppMessage("playable")` workaround is no
36+
longer needed.
37+
38+
Bot audio is rendered by attaching the remote audio track to a hidden `<audio>`
39+
element inside the `onTrackStarted` callback. See `src/app.js`.

bot-ready-signalling/client/javascript/index.html

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
</div>
2020
</div>
2121

22-
<audio id="bot-audio" autoplay></audio>
23-
2422
<div class="debug-panel">
2523
<h3>Debug Info</h3>
2624
<div id="debug-log"></div>

bot-ready-signalling/client/javascript/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"vite": "^6.3.5"
1616
},
1717
"dependencies": {
18-
"@daily-co/daily-js": "0.74.0"
18+
"@pipecat-ai/client-js": "^1.4.0",
19+
"@pipecat-ai/daily-transport": "^1.4.1"
1920
}
2021
}

bot-ready-signalling/client/javascript/src/app.js

Lines changed: 68 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -4,213 +4,153 @@
44
* SPDX-License-Identifier: BSD 2-Clause License
55
*/
66

7-
import Daily from "@daily-co/daily-js";
7+
import { PipecatClient } from "@pipecat-ai/client-js";
8+
import { DailyTransport } from "@pipecat-ai/daily-transport";
89

910
/**
1011
* ChatbotClient handles the connection and media management for a real-time
11-
* voice interaction with an AI bot.
12+
* voice interaction with an AI bot using the Pipecat client SDK.
13+
*
14+
* The bot-ready handshake is handled by RTVI: the SDK signals client-ready
15+
* automatically once the transport reaches the `ready` state, the bot replies
16+
* with `bot-ready`, and only then does the bot push its first TTS frame. No
17+
* manual sendAppMessage / "playable" plumbing is needed.
1218
*/
1319
class ChatbotClient {
1420
constructor() {
15-
// Initialize client state
16-
this.dailyCallObject = null;
21+
this.pcClient = null;
22+
this.botAudio = null;
1723
this.setupDOMElements();
1824
this.setupEventListeners();
1925
}
2026

21-
/**
22-
* Set up references to DOM elements and create necessary media elements
23-
*/
2427
setupDOMElements() {
25-
// Get references to UI control elements
2628
this.connectBtn = document.getElementById('connect-btn');
2729
this.disconnectBtn = document.getElementById('disconnect-btn');
2830
this.statusSpan = document.getElementById('connection-status');
2931
this.debugLog = document.getElementById('debug-log');
30-
31-
// Create an audio element for bot's voice output
32-
this.botAudio = document.createElement('audio');
33-
this.botAudio.autoplay = true;
34-
this.botAudio.playsInline = true;
35-
document.body.appendChild(this.botAudio);
3632
}
3733

38-
/**
39-
* Set up event listeners for connect/disconnect buttons
40-
*/
4134
setupEventListeners() {
4235
this.connectBtn.addEventListener('click', () => this.connect());
4336
this.disconnectBtn.addEventListener('click', () => this.disconnect());
4437
}
4538

46-
/**
47-
* Add a timestamped message to the debug log
48-
*/
4939
log(message) {
5040
const entry = document.createElement('div');
5141
entry.textContent = `${new Date().toISOString()} - ${message}`;
5242

53-
// Add styling based on message type
5443
if (message.startsWith('User: ')) {
55-
entry.style.color = '#2196F3'; // blue for user
44+
entry.style.color = '#2196F3';
5645
} else if (message.startsWith('Bot: ')) {
57-
entry.style.color = '#4CAF50'; // green for bot
46+
entry.style.color = '#4CAF50';
5847
}
5948

6049
this.debugLog.appendChild(entry);
6150
this.debugLog.scrollTop = this.debugLog.scrollHeight;
6251
console.log(message);
6352
}
6453

65-
/**
66-
* Update the connection status display
67-
*/
6854
updateStatus(status) {
6955
this.statusSpan.textContent = status;
7056
this.log(`Status: ${status}`);
7157
}
7258

73-
handleEventToConsole (evt) {
74-
this.log(`Received event: ${evt.action}`);
75-
};
76-
7759
/**
78-
* Set up listeners for track events (start/stop)
79-
* This handles new tracks being added during the session
60+
* Attach the bot's audio track to a hidden <audio> element so it plays back.
61+
* The Pipecat client SDK fires onTrackStarted for every remote track.
8062
*/
81-
setupTrackListeners() {
82-
if (!this.dailyCallObject) return;
83-
84-
this.dailyCallObject.on("joined-meeting", () => {
85-
this.updateStatus('Connected');
86-
this.connectBtn.disabled = true;
87-
this.disconnectBtn.disabled = false;
88-
this.log('Client connected');
89-
});
90-
this.dailyCallObject.on("track-started", (evt) => {
91-
if (evt.track.kind === "audio" && evt.participant.local === false) {
92-
this.log("Audio track started.")
93-
this.setupAudioTrack(evt.track);
94-
}
95-
});
96-
this.dailyCallObject.on("track-stopped", this.handleEventToConsole.bind(this));
97-
this.dailyCallObject.on("participant-joined", this.handleEventToConsole.bind(this));
98-
this.dailyCallObject.on("participant-updated", this.handleEventToConsole.bind(this));
99-
this.dailyCallObject.on("participant-left", () => {
100-
// When the bot leaves, we are also disconnecting from the call
101-
this.disconnect()
102-
});
103-
this.dailyCallObject.on("left-meeting", () => {
104-
this.updateStatus('Disconnected');
105-
this.connectBtn.disabled = false;
106-
this.disconnectBtn.disabled = true;
107-
this.log('Client disconnected');
108-
});
109-
this.dailyCallObject.on("error", this.handleEventToConsole.bind(this));
110-
}
63+
handleBotAudio(track, participant) {
64+
if (participant?.local || track.kind !== 'audio') return;
11165

112-
/**
113-
* Set up an audio track for playback
114-
* Handles both initial setup and track updates
115-
*/
116-
setupAudioTrack(track) {
117-
this.log(`Setting up audio track, track state: ${track.readyState}, muted: ${track.muted}`);
66+
this.log('Bot audio track started.');
11867

119-
// Check if we're already playing this track
120-
if (this.botAudio.srcObject) {
121-
const oldTrack = this.botAudio.srcObject.getAudioTracks()[0];
122-
if (oldTrack?.id === track.id) return;
68+
if (!this.botAudio) {
69+
this.botAudio = document.createElement('audio');
70+
this.botAudio.autoplay = true;
71+
this.botAudio.playsInline = true;
72+
document.body.appendChild(this.botAudio);
12373
}
124-
// Create a new MediaStream with the track and set it as the audio source
74+
12575
this.botAudio.srcObject = new MediaStream([track]);
126-
this.botAudio.onplaying = async (event) => {
127-
this.log("onplaying")
128-
this.log("Will send the audio message to play the audio at the next tick")
129-
this.dailyCallObject.sendAppMessage("playable")
130-
}
13176
}
13277

133-
async fetchRoomInfo() {
134-
let connectUrl = '/connect'
135-
let res = await fetch(connectUrl, {
136-
method: "POST",
137-
mode: "cors",
138-
headers: new Headers({
139-
"Content-Type": "application/json"
140-
}),
141-
})
142-
if (res.ok) {
143-
return res.json();
78+
removeBotAudio() {
79+
if (this.botAudio) {
80+
const stream = this.botAudio.srcObject;
81+
if (stream) {
82+
stream.getTracks().forEach((t) => t.stop());
83+
}
84+
this.botAudio.srcObject = null;
85+
this.botAudio.remove();
86+
this.botAudio = null;
14487
}
14588
}
14689

147-
/**
148-
* Initialize and connect to the bot
149-
* This sets up the RTVI client, initializes devices, and establishes the connection
150-
*/
15190
async connect() {
15291
try {
153-
// Initialize the client
154-
this.dailyCallObject = Daily.createCallObject({
155-
subscribeToTracksAutomatically: true,
92+
this.pcClient = new PipecatClient({
93+
transport: new DailyTransport(),
94+
enableMic: true,
95+
enableCam: false,
96+
callbacks: {
97+
onTransportStateChanged: (state) => {
98+
this.updateStatus(state);
99+
const isConnected = state === 'ready';
100+
this.connectBtn.disabled = state !== 'idle' && state !== 'disconnected';
101+
this.disconnectBtn.disabled = !isConnected;
102+
},
103+
onBotReady: () => {
104+
this.log('Bot ready: greeting will play next.');
105+
},
106+
onTrackStarted: (track, participant) => this.handleBotAudio(track, participant),
107+
onDisconnected: () => {
108+
this.log('Disconnected from bot.');
109+
this.removeBotAudio();
110+
this.connectBtn.disabled = false;
111+
this.disconnectBtn.disabled = true;
112+
},
113+
onError: (message) => {
114+
this.log(`Error: ${message?.data?.message || message}`);
115+
},
116+
},
156117
});
157118

158-
// Set up listeners for media track events
159-
this.setupTrackListeners();
119+
// Expose for debugging.
120+
window.pcClient = this.pcClient;
160121

161122
this.log('Creating the bot...');
162-
let roomInfo = await this.fetchRoomInfo()
163-
164-
// Connect to the bot
165-
this.log('Connecting to bot...');
166-
// Only for making debugger easier
167-
window.callObject = this.dailyCallObject;
168-
await this.dailyCallObject.join({
169-
url: roomInfo.room_url,
170-
});
171-
172-
this.log('Connection complete');
123+
await this.pcClient.startBotAndConnect({ endpoint: '/connect' });
124+
this.log('Connection complete.');
173125
} catch (error) {
174-
// Handle any errors during connection
175126
this.log(`Error connecting: ${error.message}`);
176127
this.log(`Error stack: ${error.stack}`);
177128
this.updateStatus('Error');
178129

179-
// Clean up if there's an error
180-
if (this.dailyCallObject) {
130+
if (this.pcClient) {
181131
try {
182-
await this.dailyCallObject.leave();
132+
await this.pcClient.disconnect();
183133
} catch (disconnectError) {
184134
this.log(`Error during disconnect: ${disconnectError.message}`);
185135
}
186136
}
187137
}
188138
}
189139

190-
/**
191-
* Disconnect from the bot and clean up media resources
192-
*/
193140
async disconnect() {
194-
if (this.dailyCallObject) {
141+
if (this.pcClient) {
195142
try {
196-
// Disconnect the RTVI client
197-
await this.dailyCallObject.leave();
198-
await this.dailyCallObject.destroy();
199-
this.dailyCallObject = null;
200-
201-
// Clean up audio
202-
if (this.botAudio.srcObject) {
203-
this.botAudio.srcObject.getTracks().forEach((track) => track.stop());
204-
this.botAudio.srcObject = null;
205-
}
143+
await this.pcClient.disconnect();
206144
} catch (error) {
207145
this.log(`Error disconnecting: ${error.message}`);
146+
} finally {
147+
this.pcClient = null;
148+
this.removeBotAudio();
208149
}
209150
}
210151
}
211152
}
212153

213-
// Initialize the client when the page loads
214154
window.addEventListener('DOMContentLoaded', () => {
215155
new ChatbotClient();
216156
});

bot-ready-signalling/client/react-native/README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,10 @@ Run the following command:
5656
npm run ios
5757
```
5858

59-
#### Connect to the server
60-
Use the http://localhost:5173 in your app.
59+
#### How the bot-ready handshake works
60+
61+
The Pipecat React Native client signals `client-ready` automatically once the
62+
transport reaches the `ready` state. The bot's `on_client_ready` handler then
63+
calls `set_bot_ready()` and pushes the first `TTSSpeakFrame`, so the greeting
64+
is never clipped. The previous `sendAppMessage("playable")` workaround is no
65+
longer needed.

bot-ready-signalling/client/react-native/app.json

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"expo": {
33
"name": "bot-ready-rn",
44
"slug": "bot-ready-rn",
5+
"newArchEnabled": true,
56
"version": "1.0.0",
67
"orientation": "portrait",
78
"icon": "./assets/icon.png",
@@ -54,16 +55,12 @@
5455
"favicon": "./assets/favicon.png"
5556
},
5657
"plugins": [
57-
"@config-plugins/react-native-webrtc",
5858
"@daily-co/config-plugin-rn-daily-js",
5959
[
6060
"expo-build-properties",
6161
{
6262
"android": {
63-
"minSdkVersion": 24,
64-
"compileSdkVersion": 35,
65-
"targetSdkVersion": 34,
66-
"buildToolsVersion": "35.0.0"
63+
"minSdkVersion": 24
6764
},
6865
"ios": {
6966
"deploymentTarget": "15.1"

0 commit comments

Comments
 (0)