From e6ef10338996c24090f5877279fff681ecf41781 Mon Sep 17 00:00:00 2001 From: examples-bot Date: Wed, 1 Apr 2026 06:44:15 +0000 Subject: [PATCH 1/2] fix(examples): fix async race condition in 020-twilio-media-streams-node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Twilio WebSocket message handler was registered after `await dgConnection.waitForOpen()`, causing early media events to be dropped when the Deepgram connection took time to establish. This meant no audio reached Deepgram, resulting in zero transcripts and a 30s timeout. Changes: - Make WS handler synchronous; move Deepgram setup into async IIFE - Register Twilio message handler immediately, buffer audio in mediaQueue - Flush buffered audio once Deepgram connection opens - Use sendCloseStream (SDK v5 convention) instead of sendFinalize - Remove unused twilio dependency - Close Twilio WS after stop event (matches real Twilio behavior) - Fix test to close WS after sending stop 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../package.json | 1 - .../src/index.js | 83 +++++++++++-------- .../tests/test.js | 2 +- 3 files changed, 50 insertions(+), 36 deletions(-) diff --git a/examples/020-twilio-media-streams-node/package.json b/examples/020-twilio-media-streams-node/package.json index 5251f4c..14152c8 100644 --- a/examples/020-twilio-media-streams-node/package.json +++ b/examples/020-twilio-media-streams-node/package.json @@ -12,7 +12,6 @@ "dotenv": "^16.4.0", "express": "^4.21.0", "express-ws": "^5.0.2", - "twilio": "^5.4.0", "ws": "^8.18.0" }, "engines": { diff --git a/examples/020-twilio-media-streams-node/src/index.js b/examples/020-twilio-media-streams-node/src/index.js index 5585157..cd60391 100644 --- a/examples/020-twilio-media-streams-node/src/index.js +++ b/examples/020-twilio-media-streams-node/src/index.js @@ -5,7 +5,6 @@ require('dotenv').config(); const express = require('express'); const expressWs = require('express-ws'); const { DeepgramClient } = require('@deepgram/sdk'); -const twilio = require('twilio'); const PORT = process.env.PORT || 3000; @@ -48,37 +47,14 @@ function createApp() { console.log(`[voice] New call → streaming to ${streamUrl}`); }); - app.ws('/media', async (twilioWs) => { + app.ws('/media', (twilioWs) => { let dgConnection = null; + let dgReady = false; let streamSid = null; + const mediaQueue = []; console.log('[media] Twilio WebSocket connected'); - dgConnection = await deepgram.listen.v1.createConnection(DEEPGRAM_LIVE_OPTIONS); - - dgConnection.on('open', () => { - console.log('[deepgram] Connection opened'); - }); - - dgConnection.on('error', (err) => { - console.error('[deepgram] Error:', err.message); - }); - - dgConnection.on('close', () => { - console.log('[deepgram] Connection closed'); - }); - - dgConnection.on('message', (data) => { - const transcript = data?.channel?.alternatives?.[0]?.transcript; - if (transcript) { - const tag = data.is_final ? 'final' : 'interim'; - console.log(`[${tag}] ${transcript}`); - } - }); - - dgConnection.connect(); - await dgConnection.waitForOpen(); - twilioWs.on('message', (raw) => { try { const message = JSON.parse(raw); @@ -94,21 +70,23 @@ function createApp() { break; case 'media': - try { - if (dgConnection) { + if (dgReady && dgConnection) { + try { dgConnection.sendMedia(Buffer.from(message.media.payload, 'base64')); - } - } catch {} - + } catch {} + } else { + mediaQueue.push(message.media.payload); + } break; case 'stop': console.log('[twilio] Stream stopped'); if (dgConnection) { - try { dgConnection.sendFinalize({ type: 'Finalize' }); } catch {} + try { dgConnection.sendCloseStream({ type: 'CloseStream' }); } catch {} try { dgConnection.close(); } catch {} dgConnection = null; } + twilioWs.close(); break; default: @@ -122,7 +100,7 @@ function createApp() { twilioWs.on('close', () => { console.log('[media] Twilio WebSocket closed'); if (dgConnection) { - try { dgConnection.sendFinalize({ type: 'Finalize' }); } catch {} + try { dgConnection.sendCloseStream({ type: 'CloseStream' }); } catch {} try { dgConnection.close(); } catch {} dgConnection = null; } @@ -135,6 +113,43 @@ function createApp() { dgConnection = null; } }); + + (async () => { + dgConnection = await deepgram.listen.v1.connect(DEEPGRAM_LIVE_OPTIONS); + + dgConnection.on('open', () => { + console.log('[deepgram] Connection opened'); + }); + + dgConnection.on('error', (err) => { + console.error('[deepgram] Error:', err.message); + }); + + dgConnection.on('close', () => { + console.log('[deepgram] Connection closed'); + }); + + dgConnection.on('message', (data) => { + const transcript = data?.channel?.alternatives?.[0]?.transcript; + if (transcript) { + const tag = data.is_final ? 'final' : 'interim'; + console.log(`[${tag}] ${transcript}`); + } + }); + + dgConnection.connect(); + await dgConnection.waitForOpen(); + + dgReady = true; + for (const payload of mediaQueue) { + try { + dgConnection.sendMedia(Buffer.from(payload, 'base64')); + } catch {} + } + mediaQueue.length = 0; + })().catch((err) => { + console.error('[deepgram] Setup failed:', err.message); + }); }); app.get('/', (_req, res) => { diff --git a/examples/020-twilio-media-streams-node/tests/test.js b/examples/020-twilio-media-streams-node/tests/test.js index 1a33f42..49e7675 100644 --- a/examples/020-twilio-media-streams-node/tests/test.js +++ b/examples/020-twilio-media-streams-node/tests/test.js @@ -195,8 +195,8 @@ function testMediaStreamFlow(port, audioData) { if (ws.readyState !== WebSocket.OPEN) return; if (offset >= audioData.length || offset >= MAX_BYTES) { - // 4. "stop" — call ended ws.send(JSON.stringify({ event: 'stop', streamSid: 'MZ_ci_test' })); + setTimeout(() => ws.close(), 500); return; } From a468ac932c5c0f0a0cb61e7de7346fcad4669167 Mon Sep 17 00:00:00 2001 From: examples-bot Date: Wed, 1 Apr 2026 12:14:06 +0000 Subject: [PATCH 2/2] fix(examples): use Twilio TwiML builder, fix dgReady lifecycle in 020-twilio-media-streams-node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use twilio.twiml.VoiceResponse instead of hand-built XML string - Restore twilio dependency in package.json - Set dgReady=true inside the Deepgram 'open' callback (not after waitForOpen) - Reset dgReady=false on Deepgram close/error to prevent infinite queuing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../package.json | 1 + .../src/index.js | 30 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/examples/020-twilio-media-streams-node/package.json b/examples/020-twilio-media-streams-node/package.json index 14152c8..5251f4c 100644 --- a/examples/020-twilio-media-streams-node/package.json +++ b/examples/020-twilio-media-streams-node/package.json @@ -12,6 +12,7 @@ "dotenv": "^16.4.0", "express": "^4.21.0", "express-ws": "^5.0.2", + "twilio": "^5.4.0", "ws": "^8.18.0" }, "engines": { diff --git a/examples/020-twilio-media-streams-node/src/index.js b/examples/020-twilio-media-streams-node/src/index.js index cd60391..9304304 100644 --- a/examples/020-twilio-media-streams-node/src/index.js +++ b/examples/020-twilio-media-streams-node/src/index.js @@ -5,6 +5,7 @@ require('dotenv').config(); const express = require('express'); const expressWs = require('express-ws'); const { DeepgramClient } = require('@deepgram/sdk'); +const twilio = require('twilio'); const PORT = process.env.PORT || 3000; @@ -35,15 +36,11 @@ function createApp() { const protocol = req.headers['x-forwarded-proto'] === 'https' ? 'wss' : 'ws'; const streamUrl = `${protocol}://${host}/media`; - const twiml = ` - - This call is being transcribed by Deepgram. - - - -`; + const response = new twilio.twiml.VoiceResponse(); + response.say('This call is being transcribed by Deepgram.'); + response.connect().stream({ url: streamUrl }); - res.type('text/xml').send(twiml); + res.type('text/xml').send(response.toString()); console.log(`[voice] New call → streaming to ${streamUrl}`); }); @@ -119,14 +116,23 @@ function createApp() { dgConnection.on('open', () => { console.log('[deepgram] Connection opened'); + dgReady = true; + for (const payload of mediaQueue) { + try { + dgConnection.sendMedia(Buffer.from(payload, 'base64')); + } catch {} + } + mediaQueue.length = 0; }); dgConnection.on('error', (err) => { console.error('[deepgram] Error:', err.message); + dgReady = false; }); dgConnection.on('close', () => { console.log('[deepgram] Connection closed'); + dgReady = false; }); dgConnection.on('message', (data) => { @@ -139,14 +145,6 @@ function createApp() { dgConnection.connect(); await dgConnection.waitForOpen(); - - dgReady = true; - for (const payload of mediaQueue) { - try { - dgConnection.sendMedia(Buffer.from(payload, 'base64')); - } catch {} - } - mediaQueue.length = 0; })().catch((err) => { console.error('[deepgram] Setup failed:', err.message); });