@ariaflowagents/livekit-plugin-transport-twilio 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -0
- package/dist/audio_input.d.ts +42 -0
- package/dist/audio_input.d.ts.map +1 -0
- package/dist/audio_input.js +117 -0
- package/dist/audio_input.js.map +1 -0
- package/dist/audio_output.d.ts +47 -0
- package/dist/audio_output.d.ts.map +1 -0
- package/dist/audio_output.js +162 -0
- package/dist/audio_output.js.map +1 -0
- package/dist/codec/g711.d.ts +36 -0
- package/dist/codec/g711.d.ts.map +1 -0
- package/dist/codec/g711.js +137 -0
- package/dist/codec/g711.js.map +1 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +42 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +125 -0
- package/dist/server.js.map +1 -0
- package/dist/text_output.d.ts +32 -0
- package/dist/text_output.d.ts.map +1 -0
- package/dist/text_output.js +72 -0
- package/dist/text_output.js.map +1 -0
- package/dist/transport_adapter.d.ts +95 -0
- package/dist/transport_adapter.d.ts.map +1 -0
- package/dist/transport_adapter.js +174 -0
- package/dist/transport_adapter.js.map +1 -0
- package/dist/twilio_protocol.d.ts +130 -0
- package/dist/twilio_protocol.d.ts.map +1 -0
- package/dist/twilio_protocol.js +74 -0
- package/dist/twilio_protocol.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Twilio Media Streams Transport Adapter.
|
|
3
|
+
*
|
|
4
|
+
* Connects AriaFlow agents to Twilio Media Streams for telephony
|
|
5
|
+
* voice AI applications. Works in Cloudflare Workers, Node.js, and
|
|
6
|
+
* any runtime with WebSocket support.
|
|
7
|
+
*
|
|
8
|
+
* Twilio sends G.711 μ-law audio at 8kHz. This transport handles
|
|
9
|
+
* decoding, resampling to 24kHz, and integration with AriaFlow sessions.
|
|
10
|
+
*/
|
|
11
|
+
import type { TransportAdapterConfig } from '@ariaflow/livekit-plugin';
|
|
12
|
+
import { TransportAdapter } from '@ariaflow/livekit-plugin';
|
|
13
|
+
import { TwilioAudioInput } from './audio_input.js';
|
|
14
|
+
import { TwilioAudioOutput } from './audio_output.js';
|
|
15
|
+
import { TwilioTextOutput } from './text_output.js';
|
|
16
|
+
/**
|
|
17
|
+
* Options for creating a Twilio transport adapter.
|
|
18
|
+
*/
|
|
19
|
+
export interface TwilioTransportOptions {
|
|
20
|
+
id?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Callback to send messages to the WebSocket.
|
|
23
|
+
* The transport will format messages as Twilio Media Streams events.
|
|
24
|
+
*/
|
|
25
|
+
send: (message: string) => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Transport adapter for Twilio Media Streams.
|
|
29
|
+
*
|
|
30
|
+
* This adapter bridges Twilio's Media Streams WebSocket protocol
|
|
31
|
+
* with AriaFlow's voice agent pipeline.
|
|
32
|
+
*
|
|
33
|
+
* Audio flow:
|
|
34
|
+
* - Inbound: Twilio → μ-law 8kHz → decode → resample to 24kHz → AriaFlow STT
|
|
35
|
+
* - Outbound: AriaFlow TTS → 24kHz → resample to 8kHz → μ-law encode → Twilio
|
|
36
|
+
*
|
|
37
|
+
* Usage in Cloudflare Workers:
|
|
38
|
+
* ```typescript
|
|
39
|
+
* export default {
|
|
40
|
+
* async fetch(request, env) {
|
|
41
|
+
* if (request.headers.get('Upgrade') === 'websocket') {
|
|
42
|
+
* const pair = new WebSocketPair();
|
|
43
|
+
* const [client, server] = Object.values(pair);
|
|
44
|
+
*
|
|
45
|
+
* const transport = new TwilioTransportAdapter({
|
|
46
|
+
* send: (msg) => server.send(msg),
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* server.accept();
|
|
50
|
+
*
|
|
51
|
+
* // Handle incoming messages
|
|
52
|
+
* server.addEventListener('message', (event) => {
|
|
53
|
+
* const data = JSON.parse(event.data);
|
|
54
|
+
* if (data.event === 'media') {
|
|
55
|
+
* transport.audioInput.handleMediaEvent(data);
|
|
56
|
+
* }
|
|
57
|
+
* });
|
|
58
|
+
*
|
|
59
|
+
* const { agent, sessionOptions } = createAriaFlowSession({...});
|
|
60
|
+
* await sessionManager.startSession(transport, agent, sessionOptions);
|
|
61
|
+
*
|
|
62
|
+
* return new Response(null, { status: 101, webSocket: client });
|
|
63
|
+
* }
|
|
64
|
+
* }
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare class TwilioTransportAdapter extends TransportAdapter {
|
|
69
|
+
readonly id: string;
|
|
70
|
+
readonly audioInput: TwilioAudioInput;
|
|
71
|
+
readonly audioOutput: TwilioAudioOutput;
|
|
72
|
+
readonly textOutput: TwilioTextOutput;
|
|
73
|
+
readonly config: TransportAdapterConfig;
|
|
74
|
+
private _isOpen;
|
|
75
|
+
private _streamSid;
|
|
76
|
+
constructor(options: TwilioTransportOptions);
|
|
77
|
+
/**
|
|
78
|
+
* Get the current Twilio stream SID.
|
|
79
|
+
*/
|
|
80
|
+
get streamSid(): string;
|
|
81
|
+
get isOpen(): boolean;
|
|
82
|
+
close(): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Handle an incoming message from Twilio.
|
|
85
|
+
* Call this for each WebSocket message received from Twilio.
|
|
86
|
+
*
|
|
87
|
+
* @param message - JSON string from Twilio Media Streams
|
|
88
|
+
*/
|
|
89
|
+
handleMessage(message: string): void;
|
|
90
|
+
/**
|
|
91
|
+
* Send a clear event to Twilio (clears audio buffer).
|
|
92
|
+
*/
|
|
93
|
+
clearAudio(): void;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=transport_adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport_adapter.d.ts","sourceRoot":"","sources":["../src/transport_adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAEV,sBAAsB,EACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,qBAAa,sBAAuB,SAAQ,gBAAgB;IAC1D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;IAExC,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,UAAU,CAAc;gBAEpB,OAAO,EAAE,sBAAsB;IAkC3C;;OAEG;IACH,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,IAAI,MAAM,IAAI,OAAO,CAEpB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAS5B;;;;;OAKG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAyDpC;;OAEG;IACH,UAAU,IAAI,IAAI;CAGnB"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Twilio Media Streams Transport Adapter.
|
|
3
|
+
*
|
|
4
|
+
* Connects AriaFlow agents to Twilio Media Streams for telephony
|
|
5
|
+
* voice AI applications. Works in Cloudflare Workers, Node.js, and
|
|
6
|
+
* any runtime with WebSocket support.
|
|
7
|
+
*
|
|
8
|
+
* Twilio sends G.711 μ-law audio at 8kHz. This transport handles
|
|
9
|
+
* decoding, resampling to 24kHz, and integration with AriaFlow sessions.
|
|
10
|
+
*/
|
|
11
|
+
import { TransportAdapter } from '@ariaflow/livekit-plugin';
|
|
12
|
+
import { TwilioAudioInput } from './audio_input.js';
|
|
13
|
+
import { TwilioAudioOutput } from './audio_output.js';
|
|
14
|
+
import { TwilioTextOutput } from './text_output.js';
|
|
15
|
+
/**
|
|
16
|
+
* Transport adapter for Twilio Media Streams.
|
|
17
|
+
*
|
|
18
|
+
* This adapter bridges Twilio's Media Streams WebSocket protocol
|
|
19
|
+
* with AriaFlow's voice agent pipeline.
|
|
20
|
+
*
|
|
21
|
+
* Audio flow:
|
|
22
|
+
* - Inbound: Twilio → μ-law 8kHz → decode → resample to 24kHz → AriaFlow STT
|
|
23
|
+
* - Outbound: AriaFlow TTS → 24kHz → resample to 8kHz → μ-law encode → Twilio
|
|
24
|
+
*
|
|
25
|
+
* Usage in Cloudflare Workers:
|
|
26
|
+
* ```typescript
|
|
27
|
+
* export default {
|
|
28
|
+
* async fetch(request, env) {
|
|
29
|
+
* if (request.headers.get('Upgrade') === 'websocket') {
|
|
30
|
+
* const pair = new WebSocketPair();
|
|
31
|
+
* const [client, server] = Object.values(pair);
|
|
32
|
+
*
|
|
33
|
+
* const transport = new TwilioTransportAdapter({
|
|
34
|
+
* send: (msg) => server.send(msg),
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* server.accept();
|
|
38
|
+
*
|
|
39
|
+
* // Handle incoming messages
|
|
40
|
+
* server.addEventListener('message', (event) => {
|
|
41
|
+
* const data = JSON.parse(event.data);
|
|
42
|
+
* if (data.event === 'media') {
|
|
43
|
+
* transport.audioInput.handleMediaEvent(data);
|
|
44
|
+
* }
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* const { agent, sessionOptions } = createAriaFlowSession({...});
|
|
48
|
+
* await sessionManager.startSession(transport, agent, sessionOptions);
|
|
49
|
+
*
|
|
50
|
+
* return new Response(null, { status: 101, webSocket: client });
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export class TwilioTransportAdapter extends TransportAdapter {
|
|
57
|
+
id;
|
|
58
|
+
audioInput;
|
|
59
|
+
audioOutput;
|
|
60
|
+
textOutput;
|
|
61
|
+
config;
|
|
62
|
+
_isOpen = true;
|
|
63
|
+
_streamSid = '';
|
|
64
|
+
constructor(options) {
|
|
65
|
+
super();
|
|
66
|
+
this.id = options.id || `twilio-${crypto.randomUUID()}`;
|
|
67
|
+
// Twilio uses G.711 μ-law at 8kHz, but we resample to 24kHz internally
|
|
68
|
+
this.config = {
|
|
69
|
+
sampleRate: 24000, // Internal sample rate after resampling
|
|
70
|
+
numChannels: 1,
|
|
71
|
+
encoding: 'pcm_s16le',
|
|
72
|
+
samplesPerChannel: 2400, // 100ms at 24kHz
|
|
73
|
+
};
|
|
74
|
+
// Create I/O implementations
|
|
75
|
+
this.audioInput = new TwilioAudioInput();
|
|
76
|
+
this.audioOutput = new TwilioAudioOutput();
|
|
77
|
+
this.textOutput = new TwilioTextOutput();
|
|
78
|
+
// Wire up send callbacks with streamSid tracking
|
|
79
|
+
this.audioOutput.setSendCallback((message) => {
|
|
80
|
+
// Inject current streamSid into media events
|
|
81
|
+
const event = JSON.parse(message);
|
|
82
|
+
event.streamSid = this._streamSid;
|
|
83
|
+
options.send(JSON.stringify(event));
|
|
84
|
+
});
|
|
85
|
+
this.textOutput.setSendCallback((markName) => {
|
|
86
|
+
options.send(JSON.stringify({
|
|
87
|
+
event: 'mark',
|
|
88
|
+
streamSid: this._streamSid,
|
|
89
|
+
sequenceNumber: `${Date.now()}`,
|
|
90
|
+
mark: { name: markName },
|
|
91
|
+
}));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get the current Twilio stream SID.
|
|
96
|
+
*/
|
|
97
|
+
get streamSid() {
|
|
98
|
+
return this._streamSid;
|
|
99
|
+
}
|
|
100
|
+
get isOpen() {
|
|
101
|
+
return this._isOpen;
|
|
102
|
+
}
|
|
103
|
+
async close() {
|
|
104
|
+
if (!this._isOpen)
|
|
105
|
+
return;
|
|
106
|
+
this._isOpen = false;
|
|
107
|
+
await this.audioInput.close();
|
|
108
|
+
await this.audioOutput.close();
|
|
109
|
+
await this.textOutput.close();
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Handle an incoming message from Twilio.
|
|
113
|
+
* Call this for each WebSocket message received from Twilio.
|
|
114
|
+
*
|
|
115
|
+
* @param message - JSON string from Twilio Media Streams
|
|
116
|
+
*/
|
|
117
|
+
handleMessage(message) {
|
|
118
|
+
try {
|
|
119
|
+
const event = JSON.parse(message);
|
|
120
|
+
switch (event.event) {
|
|
121
|
+
case 'media':
|
|
122
|
+
// Audio data
|
|
123
|
+
this.audioInput.handleMediaEvent(event);
|
|
124
|
+
break;
|
|
125
|
+
case 'connected':
|
|
126
|
+
// Connection established
|
|
127
|
+
console.log('[TwilioTransport] Connected to Twilio Media Streams');
|
|
128
|
+
break;
|
|
129
|
+
case 'start':
|
|
130
|
+
// Stream established - capture streamSid and ready to send audio
|
|
131
|
+
this._streamSid = event.start?.streamSid || event.streamSid || '';
|
|
132
|
+
console.log('[TwilioTransport] Stream started:', {
|
|
133
|
+
streamSid: this._streamSid,
|
|
134
|
+
callSid: event.start?.callSid,
|
|
135
|
+
tracks: event.start?.tracks,
|
|
136
|
+
});
|
|
137
|
+
break;
|
|
138
|
+
case 'stop':
|
|
139
|
+
case 'disconnected':
|
|
140
|
+
// Stream ended - flush remaining audio
|
|
141
|
+
console.log('[TwilioTransport] Stream ended:', this._streamSid);
|
|
142
|
+
this.audioInput.endCurrentStream();
|
|
143
|
+
this._isOpen = false;
|
|
144
|
+
this._streamSid = '';
|
|
145
|
+
break;
|
|
146
|
+
case 'clear':
|
|
147
|
+
// Clear audio buffer
|
|
148
|
+
console.log('[TwilioTransport] Clear audio buffer requested');
|
|
149
|
+
this.audioOutput.clearBuffer();
|
|
150
|
+
break;
|
|
151
|
+
case 'mark':
|
|
152
|
+
// Metadata mark from Twilio
|
|
153
|
+
console.log('[TwilioTransport] Received mark:', event.mark?.name);
|
|
154
|
+
break;
|
|
155
|
+
default:
|
|
156
|
+
console.log('[TwilioTransport] Unknown event:', event.event);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
console.error('[TwilioTransport] Error handling message:', {
|
|
161
|
+
error: error instanceof Error ? error.message : String(error),
|
|
162
|
+
transportId: this.id,
|
|
163
|
+
timestamp: new Date().toISOString(),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Send a clear event to Twilio (clears audio buffer).
|
|
169
|
+
*/
|
|
170
|
+
clearAudio() {
|
|
171
|
+
this.audioOutput.clearBuffer();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=transport_adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport_adapter.js","sourceRoot":"","sources":["../src/transport_adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Twilio Media Streams protocol message types and handlers.
|
|
3
|
+
*
|
|
4
|
+
* Twilio's Media Streams API sends JSON events over WebSocket:
|
|
5
|
+
* - Media events: Audio data with base64 payload
|
|
6
|
+
* - Control events: connected, disconnected, started, stopped
|
|
7
|
+
* - Metadata: marks/custom events
|
|
8
|
+
*
|
|
9
|
+
* Reference: https://www.twilio.com/docs/voice/media-streams
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Twilio Media Streams event types.
|
|
13
|
+
*/
|
|
14
|
+
export type TwilioEventType = 'connected' | 'start' | 'media' | 'stop' | 'mark' | 'clear';
|
|
15
|
+
/**
|
|
16
|
+
* Base interface for all Twilio events.
|
|
17
|
+
*/
|
|
18
|
+
export interface TwilioEvent {
|
|
19
|
+
event: TwilioEventType;
|
|
20
|
+
sequenceNumber?: string;
|
|
21
|
+
media?: {
|
|
22
|
+
track: string;
|
|
23
|
+
chunk: string;
|
|
24
|
+
timestamp: string;
|
|
25
|
+
payload: string;
|
|
26
|
+
};
|
|
27
|
+
start?: {
|
|
28
|
+
streamSid: string;
|
|
29
|
+
callSid: string;
|
|
30
|
+
tracks?: string[];
|
|
31
|
+
mediaFormat?: {
|
|
32
|
+
encoding: 'audio/x-mulaw' | 'mulaw';
|
|
33
|
+
sampleRate: 8000;
|
|
34
|
+
channels: 1;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
mark?: {
|
|
38
|
+
name: string;
|
|
39
|
+
};
|
|
40
|
+
streamSid?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Media event with audio data.
|
|
44
|
+
*/
|
|
45
|
+
export interface TwilioMediaEvent extends TwilioEvent {
|
|
46
|
+
event: 'media';
|
|
47
|
+
media: {
|
|
48
|
+
track: string;
|
|
49
|
+
chunk: string;
|
|
50
|
+
timestamp: string;
|
|
51
|
+
payload: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Connected event - stream established.
|
|
56
|
+
*/
|
|
57
|
+
export interface TwilioConnectedEvent extends TwilioEvent {
|
|
58
|
+
event: 'connected';
|
|
59
|
+
streamSid: string;
|
|
60
|
+
callSid: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Started event - media transmission began.
|
|
64
|
+
*/
|
|
65
|
+
export interface TwilioStartEvent extends TwilioEvent {
|
|
66
|
+
event: 'start';
|
|
67
|
+
start: {
|
|
68
|
+
streamSid: string;
|
|
69
|
+
callSid: string;
|
|
70
|
+
tracks?: string[];
|
|
71
|
+
mediaFormat?: {
|
|
72
|
+
encoding: 'audio/x-mulaw' | 'mulaw';
|
|
73
|
+
sampleRate: 8000;
|
|
74
|
+
channels: 1;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
streamSid?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Stopped event - media transmission ended.
|
|
81
|
+
*/
|
|
82
|
+
export interface TwilioStopEvent extends TwilioEvent {
|
|
83
|
+
event: 'stop';
|
|
84
|
+
streamSid: string;
|
|
85
|
+
callSid: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Mark event - custom metadata from TwiML.
|
|
89
|
+
*/
|
|
90
|
+
export interface TwilioMarkEvent extends TwilioEvent {
|
|
91
|
+
event: 'mark';
|
|
92
|
+
streamSid: string;
|
|
93
|
+
mark: {
|
|
94
|
+
name: string;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Clear event - clears buffered audio.
|
|
99
|
+
*/
|
|
100
|
+
export interface TwilioClearEvent extends TwilioEvent {
|
|
101
|
+
event: 'clear';
|
|
102
|
+
streamSid: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Parse a JSON message from Twilio Media Streams.
|
|
106
|
+
*
|
|
107
|
+
* @param message - Raw JSON string from WebSocket
|
|
108
|
+
* @returns Parsed Twilio event or null if invalid
|
|
109
|
+
*/
|
|
110
|
+
export declare function parseTwilioMessage(message: string): TwilioEvent | null;
|
|
111
|
+
/**
|
|
112
|
+
* Check if an event is a media event with audio data.
|
|
113
|
+
*/
|
|
114
|
+
export declare function isMediaEvent(event: TwilioEvent): event is TwilioMediaEvent;
|
|
115
|
+
/**
|
|
116
|
+
* Extract μ-law audio payload from a media event.
|
|
117
|
+
*
|
|
118
|
+
* @param event - Twilio media event
|
|
119
|
+
* @returns Base64-encoded μ-law audio data or null
|
|
120
|
+
*/
|
|
121
|
+
export declare function extractMediaPayload(event: TwilioMediaEvent): string | null;
|
|
122
|
+
/**
|
|
123
|
+
* Create a clear message to send to Twilio (clears audio buffer).
|
|
124
|
+
*/
|
|
125
|
+
export declare function createClearMessage(): string;
|
|
126
|
+
/**
|
|
127
|
+
* Create a mark message (adds metadata to the stream).
|
|
128
|
+
*/
|
|
129
|
+
export declare function createMarkMessage(name: string): string;
|
|
130
|
+
//# sourceMappingURL=twilio_protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twilio_protocol.d.ts","sourceRoot":"","sources":["../src/twilio_protocol.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Twilio Media Streams protocol message types and handlers.
|
|
3
|
+
*
|
|
4
|
+
* Twilio's Media Streams API sends JSON events over WebSocket:
|
|
5
|
+
* - Media events: Audio data with base64 payload
|
|
6
|
+
* - Control events: connected, disconnected, started, stopped
|
|
7
|
+
* - Metadata: marks/custom events
|
|
8
|
+
*
|
|
9
|
+
* Reference: https://www.twilio.com/docs/voice/media-streams
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Parse a JSON message from Twilio Media Streams.
|
|
13
|
+
*
|
|
14
|
+
* @param message - Raw JSON string from WebSocket
|
|
15
|
+
* @returns Parsed Twilio event or null if invalid
|
|
16
|
+
*/
|
|
17
|
+
export function parseTwilioMessage(message) {
|
|
18
|
+
try {
|
|
19
|
+
const event = JSON.parse(message);
|
|
20
|
+
// Validate event has required fields
|
|
21
|
+
if (!event.event) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
// Validate event type
|
|
25
|
+
const validEvents = ['connected', 'start', 'media', 'stop', 'mark', 'clear'];
|
|
26
|
+
if (!validEvents.includes(event.event)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return event;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if an event is a media event with audio data.
|
|
37
|
+
*/
|
|
38
|
+
export function isMediaEvent(event) {
|
|
39
|
+
return event.event === 'media' && event.media !== undefined && typeof event.media.payload === 'string';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Extract μ-law audio payload from a media event.
|
|
43
|
+
*
|
|
44
|
+
* @param event - Twilio media event
|
|
45
|
+
* @returns Base64-encoded μ-law audio data or null
|
|
46
|
+
*/
|
|
47
|
+
export function extractMediaPayload(event) {
|
|
48
|
+
if (event.media && event.media.payload) {
|
|
49
|
+
return event.media.payload;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create a clear message to send to Twilio (clears audio buffer).
|
|
55
|
+
*/
|
|
56
|
+
export function createClearMessage() {
|
|
57
|
+
return JSON.stringify({
|
|
58
|
+
event: 'clear',
|
|
59
|
+
streamSid: '', // Will be filled by sender
|
|
60
|
+
sequenceNumber: `${Date.now()}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create a mark message (adds metadata to the stream).
|
|
65
|
+
*/
|
|
66
|
+
export function createMarkMessage(name) {
|
|
67
|
+
return JSON.stringify({
|
|
68
|
+
event: 'mark',
|
|
69
|
+
streamSid: '', // Will be filled by sender
|
|
70
|
+
sequenceNumber: `${Date.now()}`,
|
|
71
|
+
mark: { name },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=twilio_protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twilio_protocol.js","sourceRoot":"","sources":["../src/twilio_protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA6GH;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe;IAChD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAgB,CAAC;QAEjD,qCAAqC;QACrC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YACjB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,sBAAsB;QACtB,MAAM,WAAW,GAAsB,CAAC,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QAChG,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YACvC,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,KAAkB;IAC7C,OAAO,KAAK,CAAC,KAAK,KAAK,OAAO,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,CAAC,KAAK,CAAC,OAAO,KAAK,QAAQ,CAAC;AACzG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAuB;IACzD,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACvC,OAAO,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC;IAC7B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB;IAChC,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,KAAK,EAAE,OAAO;QACd,SAAS,EAAE,EAAE,EAAE,2BAA2B;QAC1C,cAAc,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE;KAChC,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,KAAK,EAAE,MAAM;QACb,SAAS,EAAE,EAAE,EAAE,2BAA2B;QAC1C,cAAc,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE;QAC/B,IAAI,EAAE,EAAE,IAAI,EAAE;KACf,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ariaflowagents/livekit-plugin-transport-twilio",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Twilio Media Streams transport adapter for AriaFlow agents. Works with Node.js, Bun, Deno, and any WebSocket-compatible runtime.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist"],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"clean": "rm -rf dist",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"example:echo": "tsx examples/echo_bot.ts",
|
|
20
|
+
"example:server": "tsx examples/standalone_server.ts",
|
|
21
|
+
"example:hono": "tsx examples/hono_server.ts"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@ariaflowagents/livekit-plugin": "workspace:*",
|
|
26
|
+
"@livekit/agents": ">=1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@cloudflare/workers-types": "^4.20241218.0",
|
|
30
|
+
"hono": "^4.6.0",
|
|
31
|
+
"tsx": "^4.19.0",
|
|
32
|
+
"typescript": "^5.5.0",
|
|
33
|
+
"ws": "^8.18.0"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"ariaflow",
|
|
37
|
+
"livekit",
|
|
38
|
+
"twilio",
|
|
39
|
+
"voice",
|
|
40
|
+
"transport"
|
|
41
|
+
],
|
|
42
|
+
"license": "Apache-2.0"
|
|
43
|
+
}
|