Skip to content

Commit fdf995b

Browse files
examples-botclaude
andcommitted
fix: 020-twilio-media-streams-node — register Twilio WS handlers synchronously to prevent race condition
express-ws does not await async handler callbacks, so messages arriving while the Deepgram connection is being set up were silently dropped. Queue incoming media until Deepgram is ready, then flush. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8001f36 commit fdf995b

1 file changed

Lines changed: 55 additions & 32 deletions

File tree

  • examples/020-twilio-media-streams-node/src

examples/020-twilio-media-streams-node/src/index.js

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -48,37 +48,15 @@ function createApp() {
4848
console.log(`[voice] New call → streaming to ${streamUrl}`);
4949
});
5050

51-
app.ws('/media', async (twilioWs) => {
51+
app.ws('/media', (twilioWs) => {
5252
let dgConnection = null;
53+
let dgReady = false;
5354
let streamSid = null;
55+
let streamEnded = false;
56+
const mediaQueue = [];
5457

5558
console.log('[media] Twilio WebSocket connected');
5659

57-
dgConnection = await deepgram.listen.v1.createConnection(DEEPGRAM_LIVE_OPTIONS);
58-
59-
dgConnection.on('open', () => {
60-
console.log('[deepgram] Connection opened');
61-
});
62-
63-
dgConnection.on('error', (err) => {
64-
console.error('[deepgram] Error:', err.message);
65-
});
66-
67-
dgConnection.on('close', () => {
68-
console.log('[deepgram] Connection closed');
69-
});
70-
71-
dgConnection.on('message', (data) => {
72-
const transcript = data?.channel?.alternatives?.[0]?.transcript;
73-
if (transcript) {
74-
const tag = data.is_final ? 'final' : 'interim';
75-
console.log(`[${tag}] ${transcript}`);
76-
}
77-
});
78-
79-
dgConnection.connect();
80-
await dgConnection.waitForOpen();
81-
8260
twilioWs.on('message', (raw) => {
8361
try {
8462
const message = JSON.parse(raw);
@@ -94,17 +72,19 @@ function createApp() {
9472
break;
9573

9674
case 'media':
97-
try {
98-
if (dgConnection) {
75+
if (dgReady && dgConnection) {
76+
try {
9977
dgConnection.sendMedia(Buffer.from(message.media.payload, 'base64'));
100-
}
101-
} catch {}
102-
78+
} catch {}
79+
} else {
80+
mediaQueue.push(message.media.payload);
81+
}
10382
break;
10483

10584
case 'stop':
10685
console.log('[twilio] Stream stopped');
107-
if (dgConnection) {
86+
streamEnded = true;
87+
if (dgReady && dgConnection) {
10888
try { dgConnection.sendFinalize({ type: 'Finalize' }); } catch {}
10989
try { dgConnection.close(); } catch {}
11090
dgConnection = null;
@@ -135,6 +115,49 @@ function createApp() {
135115
dgConnection = null;
136116
}
137117
});
118+
119+
(async () => {
120+
dgConnection = await deepgram.listen.v1.createConnection(DEEPGRAM_LIVE_OPTIONS);
121+
122+
dgConnection.on('open', () => {
123+
console.log('[deepgram] Connection opened');
124+
});
125+
126+
dgConnection.on('error', (err) => {
127+
console.error('[deepgram] Error:', err.message);
128+
});
129+
130+
dgConnection.on('close', () => {
131+
console.log('[deepgram] Connection closed');
132+
});
133+
134+
dgConnection.on('message', (data) => {
135+
const transcript = data?.channel?.alternatives?.[0]?.transcript;
136+
if (transcript) {
137+
const tag = data.is_final ? 'final' : 'interim';
138+
console.log(`[${tag}] ${transcript}`);
139+
}
140+
});
141+
142+
dgConnection.connect();
143+
await dgConnection.waitForOpen();
144+
145+
dgReady = true;
146+
for (const payload of mediaQueue) {
147+
try {
148+
dgConnection.sendMedia(Buffer.from(payload, 'base64'));
149+
} catch {}
150+
}
151+
mediaQueue.length = 0;
152+
153+
if (streamEnded) {
154+
try { dgConnection.sendFinalize({ type: 'Finalize' }); } catch {}
155+
try { dgConnection.close(); } catch {}
156+
dgConnection = null;
157+
}
158+
})().catch((err) => {
159+
console.error('[deepgram] Setup failed:', err.message);
160+
});
138161
});
139162

140163
app.get('/', (_req, res) => {

0 commit comments

Comments
 (0)