@fonoster/streams 0.6.1-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Fonoster Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,251 @@
1
+ streams
2
+ =================
3
+
4
+ [![Streams](https://img.shields.io/badge/streams-api-brightgreen.svg)](https://fonoster.com)
5
+ [![Version](https://img.shields.io/npm/v/@fonoster/streams.svg)](https://npmjs.org/package/@fonoster/streams)
6
+ [![Downloads/week](https://img.shields.io/npm/dw/@fonoster/streams.svg)](https://npmjs.org/package/@fonoster/streams)
7
+ [![License](https://img.shields.io/npm/l/@fonoster/streams.svg)](https://github.com/fonoster/fonoster/blob/main/package.json)
8
+
9
+ This is a NodeJS implementation of the AudioSocket protocol, which is a simple protocol for accessing bidirectional audio streams from Asterisk.
10
+
11
+ * [Installation](#installation)
12
+ * [Example](#example)
13
+ * [APIs](#apis)
14
+
15
+ ## Installation
16
+
17
+ ```sh-session
18
+ $ npm install --save @fonoster/streams
19
+ ```
20
+
21
+ ## Example
22
+
23
+ While the AudioSocket is a utility for Fonoster Streams, it can be used as a standalone module. To use this library with Asterisk, you must configure your dialplan to use the `AudioSocket` application. Here is an example of how to use the AudioSocket with Asterisk:
24
+
25
+ ```
26
+ exten = 100,1,Verbose("Call to AudioSocket via Dialplan Application")
27
+ same = n,Answer()
28
+ same = n,AudioSocket(40325ec2-5efd-4bd3-805f-53576e581d13,server.example.com:9092)
29
+ same = n,Hangup()
30
+ ```
31
+
32
+ Or with the `Dial` application:
33
+
34
+ ```
35
+ exten = 100,1,Verbose("Call to AudioSocket via Dialplan Application")
36
+ same = n,Answer()
37
+ same = n,Dial(SIP/100,30,A(AudioSocket(40325ec2-5efd-4bd3-805f-53576e581d13,server.example.com:9092)))
38
+ same = n,Hangup()
39
+ ```
40
+
41
+ Connecting to the AudioSocket server using ARI with the `externalMedia` endpoint is also possible. You must ensure you set the transport to `TCP` the UUID in the data field.
42
+
43
+ > Currently, the payload towards Asterisk is limited to signed linear, 16-bit, 8kHz, mono PCM (little-endian). However, the payload from Asterisk can be changed with the `format` parameter when using ARI.
44
+
45
+ Once Asterisk is configured, you can use the `AudioSocket` class to create a server that listens for connections. Here is an example of how to use the `AudioSocket` class:
46
+
47
+ ```js
48
+ const { AudioSocket } = require("@fonoster/streams");
49
+
50
+ const audioSocket = new AudioSocket();
51
+
52
+ audioSocket.onConnection(async (req, res) => {
53
+ console.log("new connection from:", req.ref);
54
+
55
+ res.on("data", (data) => {
56
+ // Do something with the audio data
57
+ );
58
+
59
+ res.on("end", () => {
60
+ // Do something when the stream ends
61
+ });
62
+
63
+ res.on("error", (err) => {
64
+ // Do something when an error occurs
65
+ });
66
+
67
+ // Utility for playing audio files
68
+ await res.play("/path/to/audio/file");
69
+ });
70
+
71
+ audioSocket.listen(9092, () => {
72
+ console.log("server listening on port 9092");
73
+ });
74
+ ```
75
+
76
+ ## APIs
77
+
78
+ * [`AudioSocket`](#AudioSocket)
79
+ * [`AudioStream`](#AudioStream)
80
+
81
+
82
+ <a name="AudioSocket"></a>
83
+
84
+ ## AudioSocket
85
+ A NodeJS implementation of the AudioSocket protocol. The AudioSocket protocol is
86
+ a simple protocol for streaming audio from Asterisk to a NodeJS application. The protocol is
87
+ based on the I/O multiplexing model and uses
88
+
89
+ **Kind**: global class
90
+ **See**: AudioStream
91
+
92
+ * [AudioSocket](#AudioSocket)
93
+ * [new AudioSocket()](#new_AudioSocket_new)
94
+ * [.onConnection(handler)](#AudioSocket+onConnection)
95
+ * [.close()](#AudioSocket+close)
96
+
97
+ <a name="new_AudioSocket_new"></a>
98
+
99
+ ### new AudioSocket()
100
+ Constructs a new AudioSocket instance.
101
+
102
+ **Example**
103
+ ```js
104
+ const { AudioSocket } = require("@fonoster/streams");
105
+
106
+ const audioSocket = new AudioSocket();
107
+
108
+ audioSocket.onConnection(async (req, res) => {
109
+ console.log("new connection from:", req.ref);
110
+
111
+ res.on("data", (data) => {
112
+ // Do something with the audio data
113
+ );
114
+
115
+ res.on("end", () => {
116
+ // Do something when the stream ends
117
+ });
118
+
119
+ res.on("error", (err) => {
120
+ // Do something when an error occurs
121
+ });
122
+
123
+ // Utility for playing audio files
124
+ await res.play("/path/to/audio/file");
125
+ });
126
+
127
+ audioSocket.listen(9092, () => {
128
+ console.log("server listening on port 9092");
129
+ });
130
+ ```
131
+ <a name="AudioSocket+onConnection"></a>
132
+
133
+ ### audioSocket.onConnection(handler)
134
+ Sets the handler to be called when a new connection is established.
135
+
136
+ **Kind**: instance method of [<code>AudioSocket</code>](#AudioSocket)
137
+
138
+ | Param | Type | Description |
139
+ | --- | --- | --- |
140
+ | handler | <code>function</code> | The handler to call when a new connection is established |
141
+
142
+ **Example**
143
+ ```js
144
+ audioSocket.onConnection(async (req, res) => {
145
+ console.log("new connection from:", req.ref);
146
+
147
+ await res.play("/path/to/audio/file");
148
+ });
149
+ ```
150
+ <a name="AudioSocket+close"></a>
151
+
152
+ ### audioSocket.close()
153
+ Closes the server and stops listening for connections.
154
+
155
+ **Kind**: instance method of [<code>AudioSocket</code>](#AudioSocket)
156
+
157
+ <a name="AudioStream"></a>
158
+
159
+ ## AudioStream
160
+ Object representing a stream of bidirectional audio data and control messages.
161
+
162
+ **Kind**: global class
163
+
164
+ * [AudioStream](#AudioStream)
165
+ * [new AudioStream(stream, socket)](#new_AudioStream_new)
166
+ * [.write(data)](#AudioStream+write)
167
+ * [.hangup()](#AudioStream+hangup)
168
+ * [.play(filePath)](#AudioStream+play) ⇒ <code>Promise.&lt;void&gt;</code>
169
+ * [.onData(callback)](#AudioStream+onData) ⇒ [<code>AudioStream</code>](#AudioStream)
170
+ * [.onClose(callback)](#AudioStream+onClose) ⇒ [<code>AudioStream</code>](#AudioStream)
171
+ * [.onError(callback)](#AudioStream+onError) ⇒ [<code>AudioStream</code>](#AudioStream)
172
+
173
+ <a name="new_AudioStream_new"></a>
174
+
175
+ ### new AudioStream(stream, socket)
176
+ Creates a new AudioStream.
177
+
178
+
179
+ | Param | Type | Description |
180
+ | --- | --- | --- |
181
+ | stream | <code>Readable</code> | A readable stream |
182
+ | socket | <code>net.Socket</code> | A TCP socket |
183
+
184
+ <a name="AudioStream+write"></a>
185
+
186
+ ### audioStream.write(data)
187
+ Writes media data to the stream.
188
+
189
+ **Kind**: instance method of [<code>AudioStream</code>](#AudioStream)
190
+
191
+ | Param | Type | Description |
192
+ | --- | --- | --- |
193
+ | data | <code>Buffer</code> | The data to write |
194
+
195
+ <a name="AudioStream+hangup"></a>
196
+
197
+ ### audioStream.hangup()
198
+ Sends a hangup message to the stream and closes the connection.
199
+
200
+ **Kind**: instance method of [<code>AudioStream</code>](#AudioStream)
201
+ <a name="AudioStream+play"></a>
202
+
203
+ ### audioStream.play(filePath) ⇒ <code>Promise.&lt;void&gt;</code>
204
+ Utility for playing audio files.
205
+
206
+ **Kind**: instance method of [<code>AudioStream</code>](#AudioStream)
207
+
208
+ | Param | Type | Description |
209
+ | --- | --- | --- |
210
+ | filePath | <code>string</code> | The path to the audio file |
211
+
212
+ <a name="AudioStream+onData"></a>
213
+
214
+ ### audioStream.onData(callback) ⇒ [<code>AudioStream</code>](#AudioStream)
215
+ Adds a listener for the data event.
216
+
217
+ **Kind**: instance method of [<code>AudioStream</code>](#AudioStream)
218
+ **Returns**: [<code>AudioStream</code>](#AudioStream) - The AudioStream instance
219
+ **See**: EventType.DATA
220
+
221
+ | Param | Type | Description |
222
+ | --- | --- | --- |
223
+ | callback | <code>function</code> | The callback to be executed |
224
+
225
+ <a name="AudioStream+onClose"></a>
226
+
227
+ ### audioStream.onClose(callback) ⇒ [<code>AudioStream</code>](#AudioStream)
228
+ Adds a listener for the end event.
229
+
230
+ **Kind**: instance method of [<code>AudioStream</code>](#AudioStream)
231
+ **Returns**: [<code>AudioStream</code>](#AudioStream) - The AudioStream instance
232
+ **See**: EventType.END
233
+
234
+ | Param | Type | Description |
235
+ | --- | --- | --- |
236
+ | callback | <code>function</code> | The callback to be executed |
237
+
238
+ <a name="AudioStream+onError"></a>
239
+
240
+ ### audioStream.onError(callback) ⇒ [<code>AudioStream</code>](#AudioStream)
241
+ Adds a listener for the error event.
242
+
243
+ **Kind**: instance method of [<code>AudioStream</code>](#AudioStream)
244
+ **Returns**: [<code>AudioStream</code>](#AudioStream) - The AudioStream instance
245
+ **See**: EventType.ERROR
246
+
247
+ | Param | Type | Description |
248
+ | --- | --- | --- |
249
+ | callback | <code>function</code> | The callback to be executed |
250
+
251
+
@@ -0,0 +1,81 @@
1
+ import { AudioStream } from "./AudioStream";
2
+ import { StreamRequest } from "./types";
3
+ /**
4
+ * @classdesc A NodeJS implementation of the AudioSocket protocol. The AudioSocket protocol is
5
+ * a simple protocol for streaming audio from Asterisk to a NodeJS application. The protocol is
6
+ * based on the I/O multiplexing model and uses
7
+ *
8
+ * @example
9
+ *
10
+ * const { AudioSocket } = require("@fonoster/streams");
11
+ *
12
+ * const audioSocket = new AudioSocket();
13
+ *
14
+ * audioSocket.onConnection(async (req, res) => {
15
+ * console.log("new connection from:", req.ref);
16
+ *
17
+ * res.on("data", (data) => {
18
+ * // Do something with the audio data
19
+ * );
20
+ *
21
+ * res.on("end", () => {
22
+ * // Do something when the stream ends
23
+ * });
24
+ *
25
+ * res.on("error", (err) => {
26
+ * // Do something when an error occurs
27
+ * });
28
+ *
29
+ * // Utility for playing audio files
30
+ * await res.play("/path/to/audio/file");
31
+ * });
32
+ *
33
+ * audioSocket.listen(9092, () => {
34
+ * console.log("server listening on port 9092");
35
+ * });
36
+ */
37
+ declare class AudioSocket {
38
+ private server;
39
+ private connectionHandler;
40
+ /**
41
+ * Constructs a new AudioSocket instance.
42
+ *
43
+ * @see AudioStream
44
+ */
45
+ constructor();
46
+ private handleConnection;
47
+ private handleData;
48
+ /**
49
+ * Starts the server listening for connections on the specified port.
50
+ *
51
+ * @param {number} port - The port to listen on
52
+ * @param {() => void} callback - The callback to invoke when the server is listening
53
+ */
54
+ listen(port: number, callback?: () => void): void;
55
+ /**
56
+ * Starts the server listening for connections on the specified port and bind address.
57
+ *
58
+ * @param {number} port - The port to listen on
59
+ * @param {string} bind - The address to bind to
60
+ * @param {() => void} callback - The callback to invoke when the server is listening
61
+ */
62
+ listen(port: number, bind: string, callback?: () => void): void;
63
+ /**
64
+ * Sets the handler to be called when a new connection is established.
65
+ *
66
+ * @param {function(StreamRequest, AudioStream): void} handler - The handler to call when a new connection is established
67
+ * @example
68
+ *
69
+ * audioSocket.onConnection(async (req, res) => {
70
+ * console.log("new connection from:", req.ref);
71
+ *
72
+ * await res.play("/path/to/audio/file");
73
+ * });
74
+ */
75
+ onConnection(handler: (req: StreamRequest, stream: AudioStream) => void): void;
76
+ /**
77
+ * Closes the server and stops listening for connections.
78
+ */
79
+ close(): void;
80
+ }
81
+ export { AudioSocket };
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.AudioSocket = void 0;
16
+ /* eslint-disable no-dupe-class-members */
17
+ /*
18
+ * Copyright (C) 2024 by Fonoster Inc (https://fonoster.com)
19
+ * http://github.com/fonoster/fonoster
20
+ *
21
+ * This file is part of Fonoster
22
+ *
23
+ * Licensed under the MIT License (the "License");
24
+ * you may not use this file except in compliance with
25
+ * the License. You may obtain a copy of the License at
26
+ *
27
+ * https://opensource.org/licenses/MIT
28
+ *
29
+ * Unless required by applicable law or agreed to in writing, software
30
+ * distributed under the License is distributed on an "AS IS" BASIS,
31
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32
+ * See the License for the specific language governing permissions and
33
+ * limitations under the License.
34
+ */
35
+ const net_1 = __importDefault(require("net"));
36
+ const stream_1 = require("stream");
37
+ const logger_1 = require("@fonoster/logger");
38
+ const AudioSocketError_1 = require("./AudioSocketError");
39
+ const AudioStream_1 = require("./AudioStream");
40
+ const nextMessage_1 = require("./nextMessage");
41
+ const types_1 = require("./types");
42
+ const logger = (0, logger_1.getLogger)({ service: "streams", filePath: __filename });
43
+ /**
44
+ * @classdesc A NodeJS implementation of the AudioSocket protocol. The AudioSocket protocol is
45
+ * a simple protocol for streaming audio from Asterisk to a NodeJS application. The protocol is
46
+ * based on the I/O multiplexing model and uses
47
+ *
48
+ * @example
49
+ *
50
+ * const { AudioSocket } = require("@fonoster/streams");
51
+ *
52
+ * const audioSocket = new AudioSocket();
53
+ *
54
+ * audioSocket.onConnection(async (req, res) => {
55
+ * console.log("new connection from:", req.ref);
56
+ *
57
+ * res.on("data", (data) => {
58
+ * // Do something with the audio data
59
+ * );
60
+ *
61
+ * res.on("end", () => {
62
+ * // Do something when the stream ends
63
+ * });
64
+ *
65
+ * res.on("error", (err) => {
66
+ * // Do something when an error occurs
67
+ * });
68
+ *
69
+ * // Utility for playing audio files
70
+ * await res.play("/path/to/audio/file");
71
+ * });
72
+ *
73
+ * audioSocket.listen(9092, () => {
74
+ * console.log("server listening on port 9092");
75
+ * });
76
+ */
77
+ class AudioSocket {
78
+ /**
79
+ * Constructs a new AudioSocket instance.
80
+ *
81
+ * @see AudioStream
82
+ */
83
+ constructor() {
84
+ this.connectionHandler = null;
85
+ this.server = net_1.default.createServer(this.handleConnection.bind(this));
86
+ }
87
+ handleConnection(socket) {
88
+ logger.info("client connected");
89
+ const asStream = new stream_1.Readable({ read() { } });
90
+ const audioStream = new AudioStream_1.AudioStream(asStream, socket);
91
+ socket.on(types_1.EventType.DATA, (data) => this.handleData(data, asStream, audioStream));
92
+ socket.on(types_1.EventType.END, () => asStream.emit(types_1.EventType.END));
93
+ socket.on(types_1.EventType.ERROR, (err) => {
94
+ logger.error("socket error:", err);
95
+ asStream.emit(types_1.EventType.ERROR, err);
96
+ });
97
+ }
98
+ handleData(data, asStream, audioStream) {
99
+ return __awaiter(this, void 0, void 0, function* () {
100
+ const stream = new stream_1.Readable({ read() { } });
101
+ stream.push(data);
102
+ stream.push(null); // End of the stream
103
+ try {
104
+ const message = yield (0, nextMessage_1.nextMessage)(stream);
105
+ switch (message.getKind()) {
106
+ case types_1.MessageType.ID:
107
+ if (this.connectionHandler) {
108
+ this.connectionHandler({ ref: message.getId() }, audioStream);
109
+ }
110
+ else {
111
+ logger.warn("no connection handler set");
112
+ }
113
+ break;
114
+ case types_1.MessageType.SLIN:
115
+ case types_1.MessageType.SILENCE:
116
+ asStream.emit(types_1.EventType.DATA, message.getPayload());
117
+ break;
118
+ case types_1.MessageType.HANGUP:
119
+ asStream.emit(types_1.EventType.END);
120
+ break;
121
+ case types_1.MessageType.ERROR:
122
+ asStream.emit(types_1.EventType.ERROR, new AudioSocketError_1.AudioSocketError(message.getErrorCode()));
123
+ break;
124
+ default:
125
+ logger.warn("unknown message type");
126
+ break;
127
+ }
128
+ }
129
+ catch (err) {
130
+ logger.error("error processing message:", err);
131
+ }
132
+ });
133
+ }
134
+ listen(port, bindOrCallback, callback) {
135
+ const bind = typeof bindOrCallback === "string" ? bindOrCallback : "0.0.0.0";
136
+ const cb = typeof bindOrCallback === "function" ? bindOrCallback : callback;
137
+ this.server.listen(port, bind, cb);
138
+ }
139
+ /**
140
+ * Sets the handler to be called when a new connection is established.
141
+ *
142
+ * @param {function(StreamRequest, AudioStream): void} handler - The handler to call when a new connection is established
143
+ * @example
144
+ *
145
+ * audioSocket.onConnection(async (req, res) => {
146
+ * console.log("new connection from:", req.ref);
147
+ *
148
+ * await res.play("/path/to/audio/file");
149
+ * });
150
+ */
151
+ onConnection(handler) {
152
+ this.connectionHandler = handler;
153
+ }
154
+ /**
155
+ * Closes the server and stops listening for connections.
156
+ */
157
+ close() {
158
+ this.server.close();
159
+ }
160
+ }
161
+ exports.AudioSocket = AudioSocket;
@@ -0,0 +1,7 @@
1
+ import { ErrorCode } from "./types";
2
+ declare class AudioSocketError extends Error {
3
+ errorCode: ErrorCode;
4
+ constructor(errorCode: ErrorCode);
5
+ static getMessageFromCode(errorCode: ErrorCode): string;
6
+ }
7
+ export { AudioSocketError };
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AudioSocketError = void 0;
4
+ /*
5
+ * Copyright (C) 2024 by Fonoster Inc (https://fonoster.com)
6
+ * http://github.com/fonoster/fonoster
7
+ *
8
+ * This file is part of Fonoster
9
+ *
10
+ * Licensed under the MIT License (the "License");
11
+ * you may not use this file except in compliance with
12
+ * the License. You may obtain a copy of the License at
13
+ *
14
+ * https://opensource.org/licenses/MIT
15
+ *
16
+ * Unless required by applicable law or agreed to in writing, software
17
+ * distributed under the License is distributed on an "AS IS" BASIS,
18
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ * See the License for the specific language governing permissions and
20
+ * limitations under the License.
21
+ */
22
+ const types_1 = require("./types");
23
+ class AudioSocketError extends Error {
24
+ constructor(errorCode) {
25
+ super(AudioSocketError.getMessageFromCode(errorCode));
26
+ this.errorCode = errorCode;
27
+ this.name = "AudioSocketError";
28
+ }
29
+ static getMessageFromCode(errorCode) {
30
+ switch (errorCode) {
31
+ case types_1.ErrorCode.NONE:
32
+ return "No error";
33
+ case types_1.ErrorCode.AST_HANGUP:
34
+ return "Asterisk hangup";
35
+ case types_1.ErrorCode.AST_FRAME_FORWARDING:
36
+ return "Asterisk frame forwarding";
37
+ case types_1.ErrorCode.AST_MEMORY:
38
+ return "Asterisk memory";
39
+ default:
40
+ return "Unknown error";
41
+ }
42
+ }
43
+ }
44
+ exports.AudioSocketError = AudioSocketError;
@@ -0,0 +1,61 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ /// <reference types="node" />
4
+ import * as net from "net";
5
+ import { Readable } from "stream";
6
+ /**
7
+ * @classdesc Object representing a stream of bidirectional audio data and control messages.
8
+ */
9
+ declare class AudioStream {
10
+ private stream;
11
+ private socket;
12
+ /**
13
+ * Creates a new AudioStream.
14
+ *
15
+ * @param {Readable} stream - A readable stream
16
+ * @param {net.Socket} socket - A TCP socket
17
+ */
18
+ constructor(stream: Readable, socket: net.Socket);
19
+ /**
20
+ * Writes media data to the stream.
21
+ *
22
+ * @param {Buffer} data - The data to write
23
+ */
24
+ write(data: Buffer): void;
25
+ /**
26
+ * Sends a hangup message to the stream and closes the connection.
27
+ */
28
+ hangup(): void;
29
+ /**
30
+ * Utility for playing audio files.
31
+ *
32
+ * @param {string} filePath - The path to the audio file
33
+ * @return {Promise<void>}
34
+ */
35
+ play(filePath: string): Promise<void>;
36
+ /**
37
+ * Adds a listener for the data event.
38
+ *
39
+ * @param {function(Buffer): void} callback - The callback to be executed
40
+ * @return {AudioStream} The AudioStream instance
41
+ * @see EventType.DATA
42
+ */
43
+ onData(callback: (data: Buffer) => void): this;
44
+ /**
45
+ * Adds a listener for the end event.
46
+ *
47
+ * @param {function(): void} callback - The callback to be executed
48
+ * @return {AudioStream} The AudioStream instance
49
+ * @see EventType.END
50
+ */
51
+ onClose(callback: () => void): this;
52
+ /**
53
+ * Adds a listener for the error event.
54
+ *
55
+ * @param {function(Error): void} callback - The callback to be executed
56
+ * @return {AudioStream} The AudioStream instance
57
+ * @see EventType.ERROR
58
+ */
59
+ onError(callback: (err: Error) => void): this;
60
+ }
61
+ export { AudioStream };
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.AudioStream = void 0;
36
+ /*
37
+ * Copyright (C) 2024 by Fonoster Inc (https://fonoster.com)
38
+ * http://github.com/fonoster/fonoster
39
+ *
40
+ * This file is part of Fonoster
41
+ *
42
+ * Licensed under the MIT License (the "License");
43
+ * you may not use this file except in compliance with
44
+ * the License. You may obtain a copy of the License at
45
+ *
46
+ * https://opensource.org/licenses/MIT
47
+ *
48
+ * Unless required by applicable law or agreed to in writing, software
49
+ * distributed under the License is distributed on an "AS IS" BASIS,
50
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
51
+ * See the License for the specific language governing permissions and
52
+ * limitations under the License.
53
+ */
54
+ const fs = __importStar(require("fs"));
55
+ const promises_1 = require("node:timers/promises");
56
+ const Message_1 = require("./Message");
57
+ const types_1 = require("./types");
58
+ const MAX_CHUNK_SIZE = 320;
59
+ /**
60
+ * @classdesc Object representing a stream of bidirectional audio data and control messages.
61
+ */
62
+ class AudioStream {
63
+ /**
64
+ * Creates a new AudioStream.
65
+ *
66
+ * @param {Readable} stream - A readable stream
67
+ * @param {net.Socket} socket - A TCP socket
68
+ */
69
+ constructor(stream, socket) {
70
+ this.stream = stream;
71
+ this.socket = socket;
72
+ }
73
+ /**
74
+ * Writes media data to the stream.
75
+ *
76
+ * @param {Buffer} data - The data to write
77
+ */
78
+ write(data) {
79
+ const buffer = Message_1.Message.createSlinMessage(data);
80
+ this.socket.write(buffer);
81
+ }
82
+ /**
83
+ * Sends a hangup message to the stream and closes the connection.
84
+ */
85
+ hangup() {
86
+ const buffer = Message_1.Message.createHangupMessage();
87
+ this.socket.write(buffer);
88
+ this.socket.end();
89
+ this.stream.emit(types_1.EventType.END);
90
+ }
91
+ /**
92
+ * Utility for playing audio files.
93
+ *
94
+ * @param {string} filePath - The path to the audio file
95
+ * @return {Promise<void>}
96
+ */
97
+ play(filePath) {
98
+ return __awaiter(this, void 0, void 0, function* () {
99
+ const fileStream = fs.readFileSync(filePath);
100
+ let offset = 0;
101
+ // eslint-disable-next-line no-loops/no-loops
102
+ while (offset < fileStream.length) {
103
+ const sliceSize = Math.min(fileStream.length - offset, MAX_CHUNK_SIZE);
104
+ const slicedChunk = fileStream.subarray(offset, offset + sliceSize);
105
+ const buffer = Message_1.Message.createSlinMessage(slicedChunk);
106
+ this.socket.write(buffer);
107
+ offset += sliceSize;
108
+ // Wait for 20ms to match the sample rate
109
+ yield (0, promises_1.setTimeout)(20);
110
+ }
111
+ });
112
+ }
113
+ /**
114
+ * Adds a listener for the data event.
115
+ *
116
+ * @param {function(Buffer): void} callback - The callback to be executed
117
+ * @return {AudioStream} The AudioStream instance
118
+ * @see EventType.DATA
119
+ */
120
+ onData(callback) {
121
+ this.stream.on(types_1.EventType.DATA, callback);
122
+ return this;
123
+ }
124
+ /**
125
+ * Adds a listener for the end event.
126
+ *
127
+ * @param {function(): void} callback - The callback to be executed
128
+ * @return {AudioStream} The AudioStream instance
129
+ * @see EventType.END
130
+ */
131
+ onClose(callback) {
132
+ this.stream.on(types_1.EventType.END, callback);
133
+ return this;
134
+ }
135
+ /**
136
+ * Adds a listener for the error event.
137
+ *
138
+ * @param {function(Error): void} callback - The callback to be executed
139
+ * @return {AudioStream} The AudioStream instance
140
+ * @see EventType.ERROR
141
+ */
142
+ onError(callback) {
143
+ this.stream.on(types_1.EventType.ERROR, callback);
144
+ return this;
145
+ }
146
+ }
147
+ exports.AudioStream = AudioStream;
@@ -0,0 +1,16 @@
1
+ /// <reference types="node" />
2
+ import { ErrorCode, MessageType } from "./types";
3
+ declare class Message {
4
+ private data;
5
+ constructor(data: Buffer);
6
+ getContentLength(): number;
7
+ getKind(): MessageType;
8
+ getErrorCode(): ErrorCode;
9
+ getPayload(): Buffer | null;
10
+ getId(): string;
11
+ private static createMessage;
12
+ static createHangupMessage(): Buffer;
13
+ static createIDMessage(id: string): Buffer;
14
+ static createSlinMessage(data: Buffer): Buffer;
15
+ }
16
+ export { Message };
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Message = void 0;
4
+ /*
5
+ * Copyright (C) 2024 by Fonoster Inc (https://fonoster.com)
6
+ * http://github.com/fonoster/fonoster
7
+ *
8
+ * This file is part of Fonoster
9
+ *
10
+ * Licensed under the MIT License (the "License");
11
+ * you may not use this file except in compliance with
12
+ * the License. You may obtain a copy of the License at
13
+ *
14
+ * https://opensource.org/licenses/MIT
15
+ *
16
+ * Unless required by applicable law or agreed to in writing, software
17
+ * distributed under the License is distributed on an "AS IS" BASIS,
18
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ * See the License for the specific language governing permissions and
20
+ * limitations under the License.
21
+ */
22
+ const uuid_1 = require("uuid");
23
+ const types_1 = require("./types");
24
+ class Message {
25
+ constructor(data) {
26
+ this.data = data;
27
+ }
28
+ getContentLength() {
29
+ return this.data.length >= types_1.MINIMUM_MESSAGE_LENGTH
30
+ ? this.data.readUInt16BE(1)
31
+ : 0;
32
+ }
33
+ getKind() {
34
+ return this.data.length > 0 ? this.data[0] : types_1.MessageType.ERROR;
35
+ }
36
+ getErrorCode() {
37
+ if (this.getKind() !== types_1.MessageType.ERROR)
38
+ return types_1.ErrorCode.NONE;
39
+ return this.data.length >= 4
40
+ ? this.data[types_1.MINIMUM_MESSAGE_LENGTH]
41
+ : types_1.ErrorCode.UNKNOWN;
42
+ }
43
+ getPayload() {
44
+ const size = this.getContentLength();
45
+ return size > 0 ? this.data.subarray(types_1.MINIMUM_MESSAGE_LENGTH) : null;
46
+ }
47
+ getId() {
48
+ if (this.getKind() !== types_1.MessageType.ID) {
49
+ throw new Error(`Wrong message type ${this.getKind()}`);
50
+ }
51
+ return (0, uuid_1.stringify)(this.getPayload());
52
+ }
53
+ static createMessage(type, data) {
54
+ if (data.length > types_1.MAXIMUM_MESSAGE_LENGTH) {
55
+ throw new Error("Message too large");
56
+ }
57
+ const out = Buffer.alloc(types_1.MINIMUM_MESSAGE_LENGTH + data.length);
58
+ out[0] = type;
59
+ out.writeUInt16BE(data.length, 1);
60
+ data.copy(out, types_1.MINIMUM_MESSAGE_LENGTH);
61
+ return out;
62
+ }
63
+ static createHangupMessage() {
64
+ return Buffer.from([types_1.MessageType.HANGUP, 0x00, 0x00]);
65
+ }
66
+ static createIDMessage(id) {
67
+ const idBuffer = Buffer.from((0, uuid_1.parse)(id));
68
+ return this.createMessage(types_1.MessageType.ID, idBuffer);
69
+ }
70
+ static createSlinMessage(data) {
71
+ return this.createMessage(types_1.MessageType.SLIN, data);
72
+ }
73
+ }
74
+ exports.Message = Message;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ /*
13
+ * Copyright (C) 2024 by Fonoster Inc (https://fonoster.com)
14
+ * http://github.com/fonoster/fonoster
15
+ *
16
+ * This file is part of Fonoster
17
+ *
18
+ * Licensed under the MIT License (the "License");
19
+ * you may not use this file except in compliance with
20
+ * the License. You may obtain a copy of the License at
21
+ *
22
+ * https://opensource.org/licenses/MIT
23
+ *
24
+ * Unless required by applicable law or agreed to in writing, software
25
+ * distributed under the License is distributed on an "AS IS" BASIS,
26
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
27
+ * See the License for the specific language governing permissions and
28
+ * limitations under the License.
29
+ */
30
+ const logger_1 = require("@fonoster/logger");
31
+ const _1 = require(".");
32
+ const logger = (0, logger_1.getLogger)({ service: "streams", filePath: __filename });
33
+ const PORT = 9092;
34
+ const audioSocket = new _1.AudioSocket();
35
+ function connectionHandler(req, stream) {
36
+ return __awaiter(this, void 0, void 0, function* () {
37
+ const { ref } = req;
38
+ logger.verbose("new connection", { ref });
39
+ // Do something with the data (e.g. save it to a file, or send it to a transcription service)
40
+ // stream.onData((_data) => { /* save on a file or send to a transcription service */ });
41
+ stream.onClose(() => {
42
+ logger.verbose("stream closed");
43
+ });
44
+ stream.onError((err) => {
45
+ logger.error("stream error", err);
46
+ });
47
+ const filePath = process.cwd() + "/etc/sounds/test.sln";
48
+ logger.verbose("playing sound", { filePath });
49
+ yield stream.play(filePath);
50
+ // Hangup the stream after 10 seconds
51
+ setTimeout(() => __awaiter(this, void 0, void 0, function* () {
52
+ logger.verbose("hangin up the stream", { ref });
53
+ stream.hangup();
54
+ }), 10000);
55
+ });
56
+ }
57
+ audioSocket.listen(PORT, () => {
58
+ logger.info(`audiosocket listening on port ${PORT}`);
59
+ });
60
+ audioSocket.onConnection(connectionHandler);
@@ -0,0 +1,3 @@
1
+ export * from "./AudioSocket";
2
+ export * from "./AudioStream";
3
+ export * from "./types";
package/dist/index.js ADDED
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ /*
18
+ * Copyright (C) 2024 by Fonoster Inc (https://fonoster.com)
19
+ * http://github.com/fonoster/fonoster
20
+ *
21
+ * This file is part of Fonoster
22
+ *
23
+ * Licensed under the MIT License (the "License");
24
+ * you may not use this file except in compliance with
25
+ * the License. You may obtain a copy of the License at
26
+ *
27
+ * https://opensource.org/licenses/MIT
28
+ *
29
+ * Unless required by applicable law or agreed to in writing, software
30
+ * distributed under the License is distributed on an "AS IS" BASIS,
31
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32
+ * See the License for the specific language governing permissions and
33
+ * limitations under the License.
34
+ */
35
+ __exportStar(require("./AudioSocket"), exports);
36
+ __exportStar(require("./AudioStream"), exports);
37
+ __exportStar(require("./types"), exports);
@@ -0,0 +1,5 @@
1
+ /// <reference types="node" />
2
+ import { Readable } from "stream";
3
+ import { Message } from "./Message";
4
+ declare function nextMessage(stream: Readable): Promise<Message>;
5
+ export { nextMessage };
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.nextMessage = void 0;
13
+ const Message_1 = require("./Message");
14
+ const types_1 = require("./types");
15
+ function nextMessage(stream) {
16
+ return __awaiter(this, void 0, void 0, function* () {
17
+ const hdr = Buffer.alloc(types_1.MINIMUM_MESSAGE_LENGTH);
18
+ const bytesRead = (yield stream.read(types_1.MINIMUM_MESSAGE_LENGTH));
19
+ if (bytesRead.length !== types_1.MINIMUM_MESSAGE_LENGTH) {
20
+ throw new Error(`Read wrong number of bytes (${bytesRead.length}) for header`);
21
+ }
22
+ hdr.set(bytesRead);
23
+ const payloadLen = hdr.readUInt16BE(1);
24
+ if (payloadLen < 1)
25
+ return new Message_1.Message(hdr);
26
+ const payload = Buffer.alloc(payloadLen);
27
+ const payloadRead = (yield stream.read(payloadLen));
28
+ if (payloadRead.length !== payloadLen) {
29
+ throw new Error(`Read wrong number of bytes (${payloadRead.length}) for payload`);
30
+ }
31
+ payload.set(payloadRead);
32
+ return new Message_1.Message(Buffer.concat([hdr, payload]));
33
+ });
34
+ }
35
+ exports.nextMessage = nextMessage;
@@ -0,0 +1,25 @@
1
+ declare const MINIMUM_MESSAGE_LENGTH = 3;
2
+ declare const MAXIMUM_MESSAGE_LENGTH = 65535;
3
+ declare enum MessageType {
4
+ HANGUP = 0,
5
+ ID = 1,
6
+ SILENCE = 2,
7
+ SLIN = 16,
8
+ ERROR = 255
9
+ }
10
+ declare enum ErrorCode {
11
+ NONE = 0,
12
+ AST_HANGUP = 1,
13
+ AST_FRAME_FORWARDING = 2,
14
+ AST_MEMORY = 4,
15
+ UNKNOWN = 255
16
+ }
17
+ declare enum EventType {
18
+ DATA = "data",
19
+ END = "end",
20
+ ERROR = "error"
21
+ }
22
+ type StreamRequest = {
23
+ ref: string;
24
+ };
25
+ export { StreamRequest, MessageType, EventType, ErrorCode, MINIMUM_MESSAGE_LENGTH, MAXIMUM_MESSAGE_LENGTH };
package/dist/types.js ADDED
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAXIMUM_MESSAGE_LENGTH = exports.MINIMUM_MESSAGE_LENGTH = exports.ErrorCode = exports.EventType = exports.MessageType = void 0;
4
+ /*
5
+ * Copyright (C) 2024 by Fonoster Inc (https://fonoster.com)
6
+ * http://github.com/fonoster/fonoster
7
+ *
8
+ * This file is part of Fonoster
9
+ *
10
+ * Licensed under the MIT License (the "License");
11
+ * you may not use this file except in compliance with
12
+ * the License. You may obtain a copy of the License at
13
+ *
14
+ * https://opensource.org/licenses/MIT
15
+ *
16
+ * Unless required by applicable law or agreed to in writing, software
17
+ * distributed under the License is distributed on an "AS IS" BASIS,
18
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ * See the License for the specific language governing permissions and
20
+ * limitations under the License.
21
+ */
22
+ const MINIMUM_MESSAGE_LENGTH = 3;
23
+ exports.MINIMUM_MESSAGE_LENGTH = MINIMUM_MESSAGE_LENGTH;
24
+ const MAXIMUM_MESSAGE_LENGTH = 65535;
25
+ exports.MAXIMUM_MESSAGE_LENGTH = MAXIMUM_MESSAGE_LENGTH;
26
+ var MessageType;
27
+ (function (MessageType) {
28
+ MessageType[MessageType["HANGUP"] = 0] = "HANGUP";
29
+ MessageType[MessageType["ID"] = 1] = "ID";
30
+ MessageType[MessageType["SILENCE"] = 2] = "SILENCE";
31
+ MessageType[MessageType["SLIN"] = 16] = "SLIN";
32
+ MessageType[MessageType["ERROR"] = 255] = "ERROR";
33
+ })(MessageType || (exports.MessageType = MessageType = {}));
34
+ var ErrorCode;
35
+ (function (ErrorCode) {
36
+ ErrorCode[ErrorCode["NONE"] = 0] = "NONE";
37
+ ErrorCode[ErrorCode["AST_HANGUP"] = 1] = "AST_HANGUP";
38
+ ErrorCode[ErrorCode["AST_FRAME_FORWARDING"] = 2] = "AST_FRAME_FORWARDING";
39
+ ErrorCode[ErrorCode["AST_MEMORY"] = 4] = "AST_MEMORY";
40
+ ErrorCode[ErrorCode["UNKNOWN"] = 255] = "UNKNOWN";
41
+ })(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
42
+ var EventType;
43
+ (function (EventType) {
44
+ EventType["DATA"] = "data";
45
+ EventType["END"] = "end";
46
+ EventType["ERROR"] = "error";
47
+ })(EventType || (exports.EventType = EventType = {}));
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@fonoster/streams",
3
+ "version": "0.6.1-alpha.0",
4
+ "description": "Core support for Fonoster Streams",
5
+ "author": "Pedro Sanders <psanders@fonoster.com>",
6
+ "homepage": "https://github.com/fonoster/fonoster#readme",
7
+ "license": "MIT",
8
+ "main": "dist/index",
9
+ "types": "dist/index",
10
+ "directories": {
11
+ "src": "src",
12
+ "test": "test"
13
+ },
14
+ "scripts": {
15
+ "prebuild": "rimraf ./dist tsconfig.tsbuildinfo",
16
+ "build": "tsc -b tsconfig.json",
17
+ "clean": "rimraf ./dist node_modules tsconfig.tsbuildinfo",
18
+ "generate:readme": "node ../../.scripts/gen-readme.js"
19
+ },
20
+ "dependencies": {
21
+ "@fonoster/logger": "^0.6.1-alpha.0",
22
+ "uuid": "^10.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/uuid": "^9.0.8"
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/fonoster/fonoster.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/fonoster/fonoster/issues"
39
+ },
40
+ "gitHead": "2cdd1508146747550fe048c35d9a010d04f6d3aa"
41
+ }