├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.debian ├── README.md ├── audio_streamer_glue.cpp ├── audio_streamer_glue.h ├── base64.cpp ├── base64.h ├── build-mod-audio-stream.sh ├── cmake ├── FindSpeexDSP.cmake └── Packing.cmake ├── debian ├── changelog └── copyright ├── mod_audio_stream.c └── mod_audio_stream.h /.gitignore: -------------------------------------------------------------------------------- 1 | cmake-build-debug/ 2 | .idea/ 3 | .vscode/ 4 | build/ 5 | _packages -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libs/libwsc"] 2 | path = libs/libwsc 3 | url = https://github.com/amigniter/libwsc 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | project(mod_audio_stream 3 | VERSION 1.0.0 4 | DESCRIPTION "Audio streaming module for FreeSWITCH." 5 | HOMEPAGE_URL "https://github.com/amigniter/mod_audio_stream") 6 | 7 | include(GNUInstallDirs) 8 | 9 | set(CMAKE_CXX_STANDARD 11) 10 | set(CMAKE_SHARED_LIBRARY_PREFIX "") 11 | set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g") 12 | 13 | option(ENABLE_LOCAL "Enable local compile/debug specific" OFF) 14 | if(ENABLE_LOCAL) 15 | set(ENV{PKG_CONFIG_PATH} "/usr/local/freeswitch/lib/pkgconfig:$ENV{PKG_CONFIG_PATH}") 16 | endif() 17 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") 18 | 19 | find_package(PkgConfig REQUIRED) 20 | find_package(SpeexDSP REQUIRED) 21 | 22 | pkg_check_modules(FreeSWITCH REQUIRED IMPORTED_TARGET freeswitch) 23 | pkg_get_variable(FS_MOD_DIR freeswitch modulesdir) 24 | message(STATUS "FreeSWITCH modules dir: ${FS_MOD_DIR}") 25 | 26 | add_subdirectory(libs/libwsc) 27 | 28 | add_library(mod_audio_stream SHARED 29 | mod_audio_stream.c 30 | mod_audio_stream.h 31 | audio_streamer_glue.h 32 | audio_streamer_glue.cpp 33 | base64.cpp 34 | ) 35 | 36 | set_property(TARGET mod_audio_stream PROPERTY POSITION_INDEPENDENT_CODE ON) 37 | 38 | target_link_libraries(mod_audio_stream PRIVATE 39 | PkgConfig::FreeSWITCH 40 | pthread 41 | libwsc 42 | ) 43 | 44 | if(CMAKE_BUILD_TYPE MATCHES "Release") 45 | set_target_properties(${PROJECT_NAME} 46 | PROPERTIES 47 | LINK_FLAGS_RELEASE "-s") 48 | endif() 49 | 50 | set(CPACK_COMPONENTS_ALL ${PROJECT_NAME} changelog.gz copyright) 51 | set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libspeexdsp1, openssl, zlib1g, libfreeswitch1") 52 | set(CPACK_PACKAGE_NAME "mod-audio-stream") 53 | set(CMAKE_INSTALL_DOCDIR "share/doc/${CPACK_PACKAGE_NAME}") 54 | 55 | add_custom_command( 56 | DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/debian/changelog" 57 | COMMAND gzip -cn9 "${CMAKE_CURRENT_SOURCE_DIR}/debian/changelog" > "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz" 58 | OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz" 59 | COMMENT "Processing copyright file" 60 | ) 61 | add_custom_target(changelog_target ALL DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz") 62 | 63 | add_custom_command( 64 | DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/debian/copyright" 65 | COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/debian/copyright" "${CMAKE_CURRENT_BINARY_DIR}/copyright" 66 | OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/copyright" 67 | COMMENT "Generating compressed changelog.gz" 68 | ) 69 | add_custom_target(copyright_target ALL DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/copyright") 70 | 71 | install(FILES "${CMAKE_CURRENT_BINARY_DIR}/copyright" 72 | DESTINATION "${CMAKE_INSTALL_DOCDIR}" 73 | COMPONENT copyright 74 | ) 75 | 76 | install(TARGETS ${PROJECT_NAME} 77 | COMPONENT ${PROJECT_NAME} 78 | DESTINATION ${FS_MOD_DIR}) 79 | 80 | install(FILES "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz" 81 | DESTINATION "${CMAKE_INSTALL_DOCDIR}" 82 | COMPONENT changelog.gz 83 | ) 84 | 85 | message(STATUS "Components to pack: ${CPACK_COMPONENTS_ALL}") 86 | 87 | include(Packing) 88 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 amigniter 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. 22 | -------------------------------------------------------------------------------- /README.debian: -------------------------------------------------------------------------------- 1 | [how to install from repository debian bookworm] 2 | 3 | wget -O /usr/share/keyrings/mod-audio-stream.gpg https://amigniter.github.io/mod-audio-stream/mod-audio-stream.gpg 4 | 5 | echo "deb [signed-by=/usr/share/keyrings/mod-audio-stream.gpg] https://amigniter.github.io/mod-audio-stream bookworm main" > /etc/apt/sources.list.d/mod-audio-stream.list 6 | 7 | apt-get update 8 | 9 | apt-get install mod-audio-stream 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mod_audio_stream 2 | 3 | A FreeSWITCH module that streams L16 audio from a channel to a websocket endpoint. If websocket sends back responses (eg. JSON) it can be effectively used with ASR engines such as IBM Watson etc., or any other purpose you find applicable. 4 | 5 | ### Update (22/2/2025) 6 | 7 | #### :rocket: **Introducing Bi-Directional Streaming with automatic playback** 8 | 9 | A new version `mod-audio-stream v1.0.3` has been published, featuring **raw binary stream** from the websocket. 10 | It can be downloaded from the **Releases** section (pre-release) and comes as a pre-built Debian 12 package. 11 | 12 | - Playback feature allows continuous forward streaming while the playback runs independently. 13 | - It is a **full-duplex streamer** between the caller and the websocket. 14 | - It supports **base64 encoded audio** as well as the **raw binary stream** from the websocket. 15 | - Playback can be **tracked, paused, or resumed** dynamically. 16 | 17 | :small_blue_diamond: This release is a commercial product that is available for **free use**, including commercial use, with a limitation of **10 concurrent streaming channels**. For users requiring more than 10 channels, or access to the source code, please [contact us](mailto:amsoftswitch@gmail.com) 18 | for further information and licensing options. 19 | 20 | ### About 21 | 22 | - The purpose of `mod_audio_stream` was to provide a simple, low-dependency yet effective module for streaming audio and receiving responses from a websocket server. 23 | - Introduced [libwsc](https://github.com/amigniter/libwsc), our in-house, **RFC-6455 compliant** websocket client developed specifically for `mod_audio_stream`. 24 | - Replaces [ixwebsocket](https://machinezone.github.io/IXWebSocket/), which served us well for the past few years. `libwsc` is libevent-based, extremely lightweight, and optimized for low-latency audio streaming. 25 | - This module was inspired by mod_audio_fork. 26 | 27 | ## Installation 28 | 29 | ### Dependencies 30 | It requires `libfreeswitch-dev`, `libssl-dev`, `zlib1g-dev`, `libevent-dev` and `libspeexdsp-dev` on Debian/Ubuntu which are regular packages for Freeswitch installation. 31 | ### Building 32 | After cloning please execute: **git submodule init** and **git submodule update** to initialize the submodule. 33 | #### Custom path 34 | If you built FreeSWITCH from source, eq. install dir is /usr/local/freeswitch, add path to pkgconfig: 35 | ``` 36 | export PKG_CONFIG_PATH=/usr/local/freeswitch/lib/pkgconfig 37 | ``` 38 | To build the module, from the cloned repository: 39 | ``` 40 | mkdir build && cd build 41 | cmake -DCMAKE_BUILD_TYPE=Release .. 42 | make 43 | sudo make install 44 | ``` 45 | **TLS** is `OFF` by default. To build with TLS support add `-DUSE_TLS=ON` to cmake line. 46 | 47 | #### DEB Package 48 | To build DEB package after making the module: 49 | ``` 50 | cpack -G DEB 51 | ``` 52 | Debian package will be placed in root directory `_packages` folder. 53 | 54 | ## Scripted Build & Installation 55 | 56 | ``` 57 | sudo apt-get -y install git \ 58 | && cd /usr/src/ \ 59 | && git clone https://github.com/amigniter/mod_audio_stream.git \ 60 | && cd mod_audio_stream \ 61 | && sudo bash ./build-mod-audio-stream.sh 62 | ``` 63 | 64 | ### Channel variables 65 | The following channel variables can be used to fine tune websocket connection and also configure mod_audio_stream logging: 66 | 67 | | Variable | Description | Default | 68 | | -------------------------------------- | ------------------------------------------------------- | ------- | 69 | | STREAM_MESSAGE_DEFLATE | true or 1, disables per message deflate | off | 70 | | STREAM_HEART_BEAT | number of seconds, interval to send the heart beat | off | 71 | | STREAM_SUPPRESS_LOG | true or 1, suppresses printing to log | off | 72 | | STREAM_BUFFER_SIZE | buffer duration in milliseconds, divisible by 20 | 20 | 73 | | STREAM_EXTRA_HEADERS | JSON object for additional headers in string format | none | 74 | | ~~STREAM_NO_RECONNECT~~ | true or 1, disables automatic websocket reconnection | off | 75 | | STREAM_TLS_CA_FILE | CA cert or bundle, or the special values SYSTEM or NONE | SYSTEM | 76 | | STREAM_TLS_KEY_FILE | optional client key for WSS connections | none | 77 | | STREAM_TLS_CERT_FILE | optional client cert for WSS connections | none | 78 | | STREAM_TLS_DISABLE_HOSTNAME_VALIDATION | true or 1 disable hostname check in WSS connections | false | 79 | 80 | - Per message deflate compression option is enabled by default. It can lead to a very nice bandwidth savings. To disable it set the channel var to `true|1`. 81 | - Heart beat, sent every xx seconds when there is no traffic to make sure that load balancers do not kill an idle connection. 82 | - Suppress parameter is omitted by default(false). All the responses from websocket server will be printed to the log. Not to flood the log you can suppress it by setting the value to `true|1`. Events are fired still, it only affects printing to the log. 83 | - `Buffer Size` actually represents a duration of audio chunk sent to websocket. If you want to send e.g. 100ms audio packets to your ws endpoint 84 | you would set this variable to 100. If ommited, default packet size of 20ms will be sent as grabbed from the audio channel (which is default FreeSWITCH frame size) 85 | - Extra headers should be a JSON object with key-value pairs representing additional HTTP headers. Each key should be a header name, and its corresponding value should be a string. 86 | ```json 87 | { 88 | "Header1": "Value1", 89 | "Header2": "Value2", 90 | "Header3": "Value3" 91 | } 92 | - ~~Websocket automatic reconnection is on by default. To disable it set this channel variable to true or 1.~~ 93 | - libwsc does not support automatic reconnection. 94 | - TLS (for WSS) options can be fine tuned with the `STREAM_TLS_*` channel variables: 95 | - `STREAM_TLS_CA_FILE` the ca certificate (or certificate bundle) file. By default is `SYSTEM` which means use the system defaults. 96 | Can be `NONE` which result in no peer verification. 97 | - `STREAM_TLS_CERT_FILE` optional client tls certificate file sent to the server. 98 | - `STREAM_TLS_KEY_FILE` optional client tls key file for the given certificate. 99 | - `STREAM_TLS_DISABLE_HOSTNAME_VALIDATION` if `true`, disables the check of the hostname against the peer server certificate. 100 | Defaults to `false`, which enforces hostname match with the peer certificate. 101 | 102 | ## API 103 | 104 | ### Commands 105 | The freeswitch module exposes the following API commands: 106 | 107 | ``` 108 | uuid_audio_stream start 109 | ``` 110 | Attaches a media bug and starts streaming audio (in L16 format) to the websocket server. FS default is 8k. If sampling-rate is other than 8k it will be resampled. 111 | - `uuid` - Freeswitch channel unique id 112 | - `wss-url` - websocket url `ws://` or `wss://` 113 | - `mix-type` - choice of 114 | - "mono" - single channel containing caller's audio 115 | - "mixed" - single channel containing both caller and callee audio 116 | - "stereo" - two channels with caller audio in one and callee audio in the other. 117 | - `sampling-rate` - choice of 118 | - "8k" = 8000 Hz sample rate will be generated 119 | - "16k" = 16000 Hz sample rate will be generated 120 | - `metadata` - (optional) a valid `utf-8` text to send. It will be sent the first before audio streaming starts. 121 | 122 | ``` 123 | uuid_audio_stream send_text 124 | ``` 125 | Sends a text to the websocket server. Requires a valid `utf-8` text. 126 | 127 | ``` 128 | uuid_audio_stream stop 129 | ``` 130 | Stops audio stream and closes websocket connection. If _metadata_ is provided it will be sent before the connection is closed. 131 | 132 | ``` 133 | uuid_audio_stream pause 134 | ``` 135 | Pauses audio stream 136 | 137 | ``` 138 | uuid_audio_stream resume 139 | ``` 140 | Resumes audio stream 141 | 142 | ## Events 143 | Module will generate the following event types: 144 | - `mod_audio_stream::json` 145 | - `mod_audio_stream::connect` 146 | - `mod_audio_stream::disconnect` 147 | - `mod_audio_stream::error` 148 | - `mod_audio_stream::play` 149 | 150 | ### response 151 | Message received from websocket endpoint. Json expected, but it contains whatever the websocket server's response is. 152 | #### Freeswitch event generated 153 | **Name**: mod_audio_stream::json 154 | **Body**: WebSocket server response 155 | 156 | ### connect 157 | Successfully connected to websocket server. 158 | #### Freeswitch event generated 159 | **Name**: mod_audio_stream::connect 160 | **Body**: JSON 161 | ```json 162 | { 163 | "status": "connected" 164 | } 165 | ``` 166 | 167 | ### disconnect 168 | Disconnected from websocket server. 169 | #### Freeswitch event generated 170 | **Name**: mod_audio_stream::disconnect 171 | **Body**: JSON 172 | ```json 173 | { 174 | "status": "disconnected", 175 | "message": { 176 | "code": 1000, 177 | "reason": "Normal closure" 178 | } 179 | } 180 | ``` 181 | - code: `` 182 | - reason: `` 183 | 184 | ### error 185 | There is an error with the connection. Multiple fields will be available on the event to describe the error. 186 | #### Freeswitch event generated 187 | **Name**: mod_audio_stream::error 188 | **Body**: JSON 189 | ```json 190 | { 191 | "status": "error", 192 | "message": { 193 | "code": 1, 194 | "error": "String explaining the error" 195 | } 196 | } 197 | ``` 198 | - code: `` 199 | - error: `` 200 | 201 | #### Possible `code` values 202 | 203 | | Code | Enum Name | Meaning | 204 | |:----:|:----------------------|:-----------------------------------------------------| 205 | | 1 | `IO` | I/O error when reading/writing sockets | 206 | | 2 | `INVALID_HEADER` | Server sent a malformed WebSocket header | 207 | | 3 | `SERVER_MASKED` | Server frames were masked (not allowed by spec) | 208 | | 4 | `NOT_SUPPORTED` | Requested feature (e.g. extension) not supported | 209 | | 5 | `PING_TIMEOUT` | No PONG received within timeout | 210 | | 6 | `CONNECT_FAILED` | TCP connection or DNS lookup failed | 211 | | 7 | `TLS_INIT_FAILED` | Couldn't initialize SSL/TLS context | 212 | | 8 | `SSL_HANDSHAKE_FAILED`| SSL/TLS handshake with server failed | 213 | | 9 | `SSL_ERROR` | Generic OpenSSL error (certificate, cipher, etc.) | 214 | 215 | 216 | ### play 217 | **Name**: mod_audio_stream::play 218 | **Body**: JSON 219 | 220 | Websocket server may return JSON object containing base64 encoded audio to be played by the user. To use this feature, response must follow the format: 221 | ```json 222 | { 223 | "type": "streamAudio", 224 | "data": { 225 | "audioDataType": "raw", 226 | "sampleRate": 8000, 227 | "audioData": "base64 encoded audio" 228 | } 229 | } 230 | ``` 231 | - audioDataType: `` 232 | 233 | Event generated by the module (subclass: _mod_audio_stream::play_) will be the same as the `data` element with the **file** added to it representing filePath: 234 | ```json 235 | { 236 | "audioDataType": "raw", 237 | "sampleRate": 8000, 238 | "file": "/path/to/the/file" 239 | } 240 | ``` 241 | If printing to the log is not suppressed, `response` printed to the console will look the same as the event. The original response containing base64 encoded audio is replaced because it can be quite huge. 242 | 243 | All the files generated by this feature will reside at the temp directory and will be deleted when the session is closed. 244 | -------------------------------------------------------------------------------- /audio_streamer_glue.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "mod_audio_stream.h" 4 | //#include 5 | #include "WebSocketClient.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "base64.h" 12 | 13 | #define FRAME_SIZE_8000 320 /* 1000x0.02 (20ms)= 160 x(16bit= 2 bytes) 320 frame size*/ 14 | 15 | class AudioStreamer { 16 | public: 17 | 18 | AudioStreamer(const char* uuid, const char* wsUri, responseHandler_t callback, int deflate, int heart_beat, 19 | bool suppressLog, const char* extra_headers, bool no_reconnect, 20 | const char* tls_cafile, const char* tls_keyfile, const char* tls_certfile, 21 | bool tls_disable_hostname_validation): m_sessionId(uuid), m_notify(callback), 22 | m_suppress_log(suppressLog), m_extra_headers(extra_headers), m_playFile(0){ 23 | 24 | WebSocketHeaders hdrs; 25 | WebSocketTLSOptions tls; 26 | 27 | if (m_extra_headers) { 28 | cJSON *headers_json = cJSON_Parse(m_extra_headers); 29 | if (headers_json) { 30 | cJSON *iterator = headers_json->child; 31 | while (iterator) { 32 | if (iterator->type == cJSON_String && iterator->valuestring != nullptr) { 33 | hdrs.set(iterator->string, iterator->valuestring); 34 | } 35 | iterator = iterator->next; 36 | } 37 | cJSON_Delete(headers_json); 38 | } 39 | } 40 | 41 | client.setUrl(wsUri); 42 | 43 | // Setup eventual TLS options. 44 | // tls_cafile may hold the special values 45 | // NONE, which disables validation and SYSTEM which uses 46 | // the system CAs bundle 47 | if (tls_cafile) { 48 | tls.caFile = tls_cafile; 49 | } 50 | 51 | if (tls_keyfile) { 52 | tls.keyFile = tls_keyfile; 53 | } 54 | 55 | if (tls_certfile) { 56 | tls.certFile = tls_certfile; 57 | } 58 | 59 | tls.disableHostnameValidation = tls_disable_hostname_validation; 60 | client.setTLSOptions(tls); 61 | 62 | // Optional heart beat, sent every xx seconds when there is not any traffic 63 | // to make sure that load balancers do not kill an idle connection. 64 | if(heart_beat) 65 | client.setPingInterval(heart_beat); 66 | 67 | // Per message deflate connection is enabled by default. You can tweak its parameters or disable it 68 | if(deflate) 69 | client.enableCompression(false); 70 | 71 | // Set extra headers if any 72 | if(!hdrs.empty()) 73 | client.setHeaders(hdrs); 74 | 75 | // Setup a callback to be fired when a message or an event (open, close, error) is received 76 | client.setMessageCallback([this](const std::string& message) { 77 | eventCallback(MESSAGE, message.c_str()); 78 | }); 79 | 80 | client.setOpenCallback([this]() { 81 | cJSON *root; 82 | root = cJSON_CreateObject(); 83 | cJSON_AddStringToObject(root, "status", "connected"); 84 | char *json_str = cJSON_PrintUnformatted(root); 85 | eventCallback(CONNECT_SUCCESS, json_str); 86 | cJSON_Delete(root); 87 | switch_safe_free(json_str); 88 | }); 89 | 90 | client.setErrorCallback([this](int code, const std::string &msg) { 91 | cJSON *root, *message; 92 | root = cJSON_CreateObject(); 93 | cJSON_AddStringToObject(root, "status", "error"); 94 | message = cJSON_CreateObject(); 95 | cJSON_AddNumberToObject(message, "code", code); 96 | cJSON_AddStringToObject(message, "error", msg.c_str()); 97 | cJSON_AddItemToObject(root, "message", message); 98 | 99 | char *json_str = cJSON_PrintUnformatted(root); 100 | 101 | eventCallback(CONNECT_ERROR, json_str); 102 | 103 | cJSON_Delete(root); 104 | switch_safe_free(json_str); 105 | }); 106 | 107 | client.setCloseCallback([this](int code, const std::string &reason) { 108 | cJSON *root, *message; 109 | root = cJSON_CreateObject(); 110 | cJSON_AddStringToObject(root, "status", "disconnected"); 111 | message = cJSON_CreateObject(); 112 | cJSON_AddNumberToObject(message, "code", code); 113 | cJSON_AddStringToObject(message, "reason", reason.c_str()); 114 | cJSON_AddItemToObject(root, "message", message); 115 | char *json_str = cJSON_PrintUnformatted(root); 116 | 117 | eventCallback(CONNECTION_DROPPED, json_str); 118 | 119 | cJSON_Delete(root); 120 | switch_safe_free(json_str); 121 | }); 122 | 123 | // Now that our callback is setup, we can start our background thread and receive messages 124 | client.connect(); 125 | } 126 | 127 | switch_media_bug_t *get_media_bug(switch_core_session_t *session) { 128 | switch_channel_t *channel = switch_core_session_get_channel(session); 129 | if(!channel) { 130 | return nullptr; 131 | } 132 | auto *bug = (switch_media_bug_t *) switch_channel_get_private(channel, MY_BUG_NAME); 133 | return bug; 134 | } 135 | 136 | inline void media_bug_close(switch_core_session_t *session) { 137 | auto *bug = get_media_bug(session); 138 | if(bug) { 139 | auto* tech_pvt = (private_t*) switch_core_media_bug_get_user_data(bug); 140 | tech_pvt->close_requested = 1; 141 | switch_core_media_bug_close(&bug, SWITCH_FALSE); 142 | } 143 | } 144 | 145 | inline void send_initial_metadata(switch_core_session_t *session) { 146 | auto *bug = get_media_bug(session); 147 | if(bug) { 148 | auto* tech_pvt = (private_t*) switch_core_media_bug_get_user_data(bug); 149 | if(tech_pvt && strlen(tech_pvt->initialMetadata) > 0) { 150 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, 151 | "sending initial metadata %s\n", tech_pvt->initialMetadata); 152 | writeText(tech_pvt->initialMetadata); 153 | } 154 | } 155 | } 156 | 157 | void eventCallback(notifyEvent_t event, const char* message) { 158 | switch_core_session_t* psession = switch_core_session_locate(m_sessionId.c_str()); 159 | if(psession) { 160 | switch (event) { 161 | case CONNECT_SUCCESS: 162 | send_initial_metadata(psession); 163 | m_notify(psession, EVENT_CONNECT, message); 164 | break; 165 | case CONNECTION_DROPPED: 166 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(psession), SWITCH_LOG_INFO, "connection closed\n"); 167 | m_notify(psession, EVENT_DISCONNECT, message); 168 | break; 169 | case CONNECT_ERROR: 170 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(psession), SWITCH_LOG_INFO, "connection error\n"); 171 | m_notify(psession, EVENT_ERROR, message); 172 | 173 | media_bug_close(psession); 174 | 175 | break; 176 | case MESSAGE: 177 | std::string msg(message); 178 | if(processMessage(psession, msg) != SWITCH_TRUE) { 179 | m_notify(psession, EVENT_JSON, msg.c_str()); 180 | } 181 | if(!m_suppress_log) 182 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(psession), SWITCH_LOG_DEBUG, "response: %s\n", msg.c_str()); 183 | break; 184 | } 185 | switch_core_session_rwunlock(psession); 186 | } 187 | } 188 | 189 | switch_bool_t processMessage(switch_core_session_t* session, std::string& message) { 190 | cJSON* json = cJSON_Parse(message.c_str()); 191 | switch_bool_t status = SWITCH_FALSE; 192 | if (!json) { 193 | return status; 194 | } 195 | const char* jsType = cJSON_GetObjectCstr(json, "type"); 196 | if(jsType && strcmp(jsType, "streamAudio") == 0) { 197 | cJSON* jsonData = cJSON_GetObjectItem(json, "data"); 198 | if(jsonData) { 199 | cJSON* jsonFile = nullptr; 200 | cJSON* jsonAudio = cJSON_DetachItemFromObject(jsonData, "audioData"); 201 | const char* jsAudioDataType = cJSON_GetObjectCstr(jsonData, "audioDataType"); 202 | std::string fileType; 203 | int sampleRate; 204 | if (0 == strcmp(jsAudioDataType, "raw")) { 205 | cJSON* jsonSampleRate = cJSON_GetObjectItem(jsonData, "sampleRate"); 206 | sampleRate = jsonSampleRate && jsonSampleRate->valueint ? jsonSampleRate->valueint : 0; 207 | std::unordered_map sampleRateMap = { 208 | {8000, ".r8"}, 209 | {16000, ".r16"}, 210 | {24000, ".r24"}, 211 | {32000, ".r32"}, 212 | {48000, ".r48"}, 213 | {64000, ".r64"} 214 | }; 215 | auto it = sampleRateMap.find(sampleRate); 216 | fileType = (it != sampleRateMap.end()) ? it->second : ""; 217 | } else if (0 == strcmp(jsAudioDataType, "wav")) { 218 | fileType = ".wav"; 219 | } else if (0 == strcmp(jsAudioDataType, "mp3")) { 220 | fileType = ".mp3"; 221 | } else if (0 == strcmp(jsAudioDataType, "ogg")) { 222 | fileType = ".ogg"; 223 | } else { 224 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "(%s) processMessage - unsupported audio type: %s\n", 225 | m_sessionId.c_str(), jsAudioDataType); 226 | } 227 | 228 | if(jsonAudio && jsonAudio->valuestring != nullptr && !fileType.empty()) { 229 | char filePath[256]; 230 | std::string rawAudio; 231 | try { 232 | rawAudio = base64_decode(jsonAudio->valuestring); 233 | } catch (const std::exception& e) { 234 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "(%s) processMessage - base64 decode error: %s\n", 235 | m_sessionId.c_str(), e.what()); 236 | cJSON_Delete(jsonAudio); cJSON_Delete(json); 237 | return status; 238 | } 239 | switch_snprintf(filePath, 256, "%s%s%s_%d.tmp%s", SWITCH_GLOBAL_dirs.temp_dir, 240 | SWITCH_PATH_SEPARATOR, m_sessionId.c_str(), m_playFile++, fileType.c_str()); 241 | std::ofstream fstream(filePath, std::ofstream::binary); 242 | fstream << rawAudio; 243 | fstream.close(); 244 | m_Files.insert(filePath); 245 | jsonFile = cJSON_CreateString(filePath); 246 | cJSON_AddItemToObject(jsonData, "file", jsonFile); 247 | } 248 | 249 | if(jsonFile) { 250 | char *jsonString = cJSON_PrintUnformatted(jsonData); 251 | m_notify(session, EVENT_PLAY, jsonString); 252 | message.assign(jsonString); 253 | free(jsonString); 254 | status = SWITCH_TRUE; 255 | } 256 | if (jsonAudio) 257 | cJSON_Delete(jsonAudio); 258 | 259 | } else { 260 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "(%s) processMessage - no data in streamAudio\n", m_sessionId.c_str()); 261 | } 262 | } 263 | cJSON_Delete(json); 264 | return status; 265 | } 266 | 267 | ~AudioStreamer()= default; 268 | 269 | void disconnect() { 270 | switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "disconnecting...\n"); 271 | client.disconnect(); 272 | } 273 | 274 | bool isConnected() { 275 | return client.isConnected(); 276 | } 277 | 278 | void writeBinary(uint8_t* buffer, size_t len) { 279 | if(!this->isConnected()) return; 280 | client.sendBinary(buffer, len); 281 | } 282 | 283 | void writeText(const char* text) { 284 | if(!this->isConnected()) return; 285 | client.sendMessage(text, strlen(text)); 286 | } 287 | 288 | void deleteFiles() { 289 | if(m_playFile >0) { 290 | for (const auto &fileName: m_Files) { 291 | remove(fileName.c_str()); 292 | } 293 | } 294 | } 295 | 296 | private: 297 | std::string m_sessionId; 298 | responseHandler_t m_notify; 299 | WebSocketClient client; 300 | bool m_suppress_log; 301 | const char* m_extra_headers; 302 | int m_playFile; 303 | std::unordered_set m_Files; 304 | }; 305 | 306 | 307 | namespace { 308 | 309 | switch_status_t stream_data_init(private_t *tech_pvt, switch_core_session_t *session, char *wsUri, 310 | uint32_t sampling, int desiredSampling, int channels, char *metadata, responseHandler_t responseHandler, 311 | int deflate, int heart_beat, bool suppressLog, int rtp_packets, const char* extra_headers, 312 | bool no_reconnect, const char *tls_cafile, const char *tls_keyfile, 313 | const char *tls_certfile, bool tls_disable_hostname_validation) 314 | { 315 | int err; //speex 316 | 317 | switch_memory_pool_t *pool = switch_core_session_get_pool(session); 318 | 319 | memset(tech_pvt, 0, sizeof(private_t)); 320 | 321 | strncpy(tech_pvt->sessionId, switch_core_session_get_uuid(session), MAX_SESSION_ID); 322 | strncpy(tech_pvt->ws_uri, wsUri, MAX_WS_URI); 323 | tech_pvt->sampling = desiredSampling; 324 | tech_pvt->responseHandler = responseHandler; 325 | tech_pvt->rtp_packets = rtp_packets; 326 | tech_pvt->channels = channels; 327 | tech_pvt->audio_paused = 0; 328 | 329 | if (metadata) strncpy(tech_pvt->initialMetadata, metadata, MAX_METADATA_LEN); 330 | 331 | //size_t buflen = (FRAME_SIZE_8000 * desiredSampling / 8000 * channels * 1000 / RTP_PERIOD * BUFFERED_SEC); 332 | const size_t buflen = (FRAME_SIZE_8000 * desiredSampling / 8000 * channels * rtp_packets); 333 | 334 | auto* as = new AudioStreamer(tech_pvt->sessionId, wsUri, responseHandler, deflate, heart_beat, 335 | suppressLog, extra_headers, no_reconnect, 336 | tls_cafile, tls_keyfile, tls_certfile, tls_disable_hostname_validation); 337 | 338 | tech_pvt->pAudioStreamer = static_cast(as); 339 | 340 | switch_mutex_init(&tech_pvt->mutex, SWITCH_MUTEX_NESTED, pool); 341 | 342 | if (switch_buffer_create(pool, &tech_pvt->sbuffer, buflen) != SWITCH_STATUS_SUCCESS) { 343 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, 344 | "%s: Error creating switch buffer.\n", tech_pvt->sessionId); 345 | return SWITCH_STATUS_FALSE; 346 | } 347 | 348 | if (desiredSampling != sampling) { 349 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "(%s) resampling from %u to %u\n", tech_pvt->sessionId, sampling, desiredSampling); 350 | tech_pvt->resampler = speex_resampler_init(channels, sampling, desiredSampling, SWITCH_RESAMPLE_QUALITY, &err); 351 | if (0 != err) { 352 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "Error initializing resampler: %s.\n", speex_resampler_strerror(err)); 353 | return SWITCH_STATUS_FALSE; 354 | } 355 | } 356 | else { 357 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "(%s) no resampling needed for this call\n", tech_pvt->sessionId); 358 | } 359 | 360 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "(%s) stream_data_init\n", tech_pvt->sessionId); 361 | 362 | return SWITCH_STATUS_SUCCESS; 363 | } 364 | 365 | void destroy_tech_pvt(private_t* tech_pvt) { 366 | switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "%s destroy_tech_pvt\n", tech_pvt->sessionId); 367 | if (tech_pvt->resampler) { 368 | speex_resampler_destroy(tech_pvt->resampler); 369 | tech_pvt->resampler = nullptr; 370 | } 371 | if (tech_pvt->mutex) { 372 | switch_mutex_destroy(tech_pvt->mutex); 373 | tech_pvt->mutex = nullptr; 374 | } 375 | if (tech_pvt->pAudioStreamer) { 376 | auto* as = (AudioStreamer *) tech_pvt->pAudioStreamer; 377 | delete as; 378 | tech_pvt->pAudioStreamer = nullptr; 379 | } 380 | } 381 | 382 | void finish(private_t* tech_pvt) { 383 | std::shared_ptr aStreamer; 384 | aStreamer.reset((AudioStreamer *)tech_pvt->pAudioStreamer); 385 | tech_pvt->pAudioStreamer = nullptr; 386 | 387 | std::thread t([aStreamer]{ 388 | aStreamer->disconnect(); 389 | }); 390 | t.detach(); 391 | } 392 | 393 | } 394 | 395 | extern "C" { 396 | int validate_ws_uri(const char* url, char* wsUri) { 397 | const char* scheme = nullptr; 398 | const char* hostStart = nullptr; 399 | const char* hostEnd = nullptr; 400 | const char* portStart = nullptr; 401 | 402 | // Check scheme 403 | if (strncmp(url, "ws://", 5) == 0) { 404 | scheme = "ws"; 405 | hostStart = url + 5; 406 | } else if (strncmp(url, "wss://", 6) == 0) { 407 | scheme = "wss"; 408 | hostStart = url + 6; 409 | } else { 410 | return 0; 411 | } 412 | 413 | // Find host end or port start 414 | hostEnd = hostStart; 415 | while (*hostEnd && *hostEnd != ':' && *hostEnd != '/') { 416 | if (!std::isalnum(*hostEnd) && *hostEnd != '-' && *hostEnd != '.') { 417 | return 0; 418 | } 419 | ++hostEnd; 420 | } 421 | 422 | // Check if host is empty 423 | if (hostStart == hostEnd) { 424 | return 0; 425 | } 426 | 427 | // Check for port 428 | if (*hostEnd == ':') { 429 | portStart = hostEnd + 1; 430 | while (*portStart && *portStart != '/') { 431 | if (!std::isdigit(*portStart)) { 432 | return 0; 433 | } 434 | ++portStart; 435 | } 436 | } 437 | 438 | // Copy valid URI to wsUri 439 | std::strncpy(wsUri, url, MAX_WS_URI); 440 | return 1; 441 | } 442 | 443 | switch_status_t is_valid_utf8(const char *str) { 444 | switch_status_t status = SWITCH_STATUS_FALSE; 445 | while (*str) { 446 | if ((*str & 0x80) == 0x00) { 447 | // 1-byte character 448 | str++; 449 | } else if ((*str & 0xE0) == 0xC0) { 450 | // 2-byte character 451 | if ((str[1] & 0xC0) != 0x80) { 452 | return status; 453 | } 454 | str += 2; 455 | } else if ((*str & 0xF0) == 0xE0) { 456 | // 3-byte character 457 | if ((str[1] & 0xC0) != 0x80 || (str[2] & 0xC0) != 0x80) { 458 | return status; 459 | } 460 | str += 3; 461 | } else if ((*str & 0xF8) == 0xF0) { 462 | // 4-byte character 463 | if ((str[1] & 0xC0) != 0x80 || (str[2] & 0xC0) != 0x80 || (str[3] & 0xC0) != 0x80) { 464 | return status; 465 | } 466 | str += 4; 467 | } else { 468 | // invalid character 469 | return status; 470 | } 471 | } 472 | return SWITCH_STATUS_SUCCESS; 473 | } 474 | 475 | switch_status_t stream_session_send_text(switch_core_session_t *session, char* text) { 476 | switch_channel_t *channel = switch_core_session_get_channel(session); 477 | auto *bug = (switch_media_bug_t*) switch_channel_get_private(channel, MY_BUG_NAME); 478 | if (!bug) { 479 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "stream_session_send_text failed because no bug\n"); 480 | return SWITCH_STATUS_FALSE; 481 | } 482 | auto *tech_pvt = (private_t*) switch_core_media_bug_get_user_data(bug); 483 | 484 | if (!tech_pvt) return SWITCH_STATUS_FALSE; 485 | auto *pAudioStreamer = static_cast(tech_pvt->pAudioStreamer); 486 | if (pAudioStreamer && text) pAudioStreamer->writeText(text); 487 | 488 | return SWITCH_STATUS_SUCCESS; 489 | } 490 | 491 | switch_status_t stream_session_pauseresume(switch_core_session_t *session, int pause) { 492 | switch_channel_t *channel = switch_core_session_get_channel(session); 493 | auto *bug = (switch_media_bug_t*) switch_channel_get_private(channel, MY_BUG_NAME); 494 | if (!bug) { 495 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "stream_session_pauseresume failed because no bug\n"); 496 | return SWITCH_STATUS_FALSE; 497 | } 498 | auto *tech_pvt = (private_t*) switch_core_media_bug_get_user_data(bug); 499 | 500 | if (!tech_pvt) return SWITCH_STATUS_FALSE; 501 | 502 | switch_core_media_bug_flush(bug); 503 | tech_pvt->audio_paused = pause; 504 | return SWITCH_STATUS_SUCCESS; 505 | } 506 | 507 | switch_status_t stream_session_init(switch_core_session_t *session, 508 | responseHandler_t responseHandler, 509 | uint32_t samples_per_second, 510 | char *wsUri, 511 | int sampling, 512 | int channels, 513 | char* metadata, 514 | void **ppUserData) 515 | { 516 | int deflate, heart_beat; 517 | bool suppressLog = false; 518 | const char* buffer_size; 519 | const char* extra_headers; 520 | int rtp_packets = 1; //20ms burst 521 | bool no_reconnect = false; 522 | const char* tls_cafile = NULL;; 523 | const char* tls_keyfile = NULL;; 524 | const char* tls_certfile = NULL;; 525 | bool tls_disable_hostname_validation = false; 526 | 527 | switch_channel_t *channel = switch_core_session_get_channel(session); 528 | 529 | if (switch_channel_var_true(channel, "STREAM_MESSAGE_DEFLATE")) { 530 | deflate = 1; 531 | } 532 | 533 | if (switch_channel_var_true(channel, "STREAM_SUPPRESS_LOG")) { 534 | suppressLog = true; 535 | } 536 | 537 | if (switch_channel_var_true(channel, "STREAM_NO_RECONNECT")) { 538 | no_reconnect = true; 539 | } 540 | 541 | tls_cafile = switch_channel_get_variable(channel, "STREAM_TLS_CA_FILE"); 542 | tls_keyfile = switch_channel_get_variable(channel, "STREAM_TLS_KEY_FILE"); 543 | tls_certfile = switch_channel_get_variable(channel, "STREAM_TLS_CERT_FILE"); 544 | 545 | if (switch_channel_var_true(channel, "STREAM_TLS_DISABLE_HOSTNAME_VALIDATION")) { 546 | tls_disable_hostname_validation = true; 547 | } 548 | 549 | const char* heartBeat = switch_channel_get_variable(channel, "STREAM_HEART_BEAT"); 550 | if (heartBeat) { 551 | char *endptr; 552 | long value = strtol(heartBeat, &endptr, 10); 553 | if (*endptr == '\0' && value <= INT_MAX && value >= INT_MIN) { 554 | heart_beat = (int) value; 555 | } 556 | } 557 | 558 | if ((buffer_size = switch_channel_get_variable(channel, "STREAM_BUFFER_SIZE"))) { 559 | int bSize = atoi(buffer_size); 560 | if(bSize % 20 != 0) { 561 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_WARNING, "%s: Buffer size of %s is not a multiple of 20ms. Using default 20ms.\n", 562 | switch_channel_get_name(channel), buffer_size); 563 | } else if(bSize >= 20){ 564 | rtp_packets = bSize/20; 565 | } 566 | } 567 | 568 | extra_headers = switch_channel_get_variable(channel, "STREAM_EXTRA_HEADERS"); 569 | 570 | // allocate per-session tech_pvt 571 | auto* tech_pvt = (private_t *) switch_core_session_alloc(session, sizeof(private_t)); 572 | 573 | if (!tech_pvt) { 574 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "error allocating memory!\n"); 575 | return SWITCH_STATUS_FALSE; 576 | } 577 | if (SWITCH_STATUS_SUCCESS != stream_data_init(tech_pvt, session, wsUri, samples_per_second, sampling, channels, metadata, responseHandler, deflate, heart_beat, 578 | suppressLog, rtp_packets, extra_headers, no_reconnect, tls_cafile, tls_keyfile, tls_certfile, tls_disable_hostname_validation)) { 579 | destroy_tech_pvt(tech_pvt); 580 | return SWITCH_STATUS_FALSE; 581 | } 582 | 583 | *ppUserData = tech_pvt; 584 | 585 | return SWITCH_STATUS_SUCCESS; 586 | } 587 | 588 | switch_bool_t stream_frame(switch_media_bug_t *bug) { 589 | auto *tech_pvt = (private_t *)switch_core_media_bug_get_user_data(bug); 590 | if (!tech_pvt || tech_pvt->audio_paused) return SWITCH_TRUE; 591 | 592 | if (switch_mutex_trylock(tech_pvt->mutex) != SWITCH_STATUS_SUCCESS) { 593 | return SWITCH_TRUE; 594 | } 595 | 596 | auto *pAudioStreamer = static_cast(tech_pvt->pAudioStreamer); 597 | 598 | if (!pAudioStreamer || !pAudioStreamer->isConnected()) { 599 | switch_mutex_unlock(tech_pvt->mutex); 600 | return SWITCH_TRUE; 601 | } 602 | 603 | auto flush_sbuffer = [&]() { 604 | switch_size_t inuse = switch_buffer_inuse(tech_pvt->sbuffer); 605 | if (inuse > 0) { 606 | std::vector tmp(inuse); 607 | switch_buffer_read(tech_pvt->sbuffer, tmp.data(), inuse); 608 | switch_buffer_zero(tech_pvt->sbuffer); 609 | pAudioStreamer->writeBinary(tmp.data(), inuse); 610 | } 611 | }; 612 | 613 | uint8_t data_buf[SWITCH_RECOMMENDED_BUFFER_SIZE]; 614 | switch_frame_t frame = {0}; 615 | frame.data = data_buf; 616 | frame.buflen = SWITCH_RECOMMENDED_BUFFER_SIZE; 617 | 618 | while (switch_core_media_bug_read(bug, &frame, SWITCH_TRUE) == SWITCH_STATUS_SUCCESS) { 619 | if (!tech_pvt->resampler) { 620 | if (tech_pvt->rtp_packets == 1) { 621 | pAudioStreamer->writeBinary((uint8_t *)frame.data, frame.datalen); 622 | } else { 623 | size_t write_len = frame.datalen; 624 | const uint8_t *write_data = (const uint8_t *)frame.data; 625 | switch_size_t free_space = switch_buffer_freespace(tech_pvt->sbuffer); 626 | if (write_len > free_space) { 627 | flush_sbuffer(); 628 | } 629 | switch_buffer_write(tech_pvt->sbuffer, write_data, write_len); 630 | if (switch_buffer_freespace(tech_pvt->sbuffer) == 0) { 631 | flush_sbuffer(); 632 | } 633 | } 634 | continue; 635 | } 636 | 637 | size_t available = switch_buffer_freespace(tech_pvt->sbuffer); 638 | spx_uint32_t in_len = frame.samples; 639 | spx_uint32_t out_len = available / (tech_pvt->channels * sizeof(spx_int16_t)); 640 | if (out_len == 0) { 641 | flush_sbuffer(); 642 | available = switch_buffer_freespace(tech_pvt->sbuffer); 643 | out_len = available / (tech_pvt->channels * sizeof(spx_int16_t)); 644 | } 645 | 646 | spx_int16_t outbuf[out_len * tech_pvt->channels]; 647 | 648 | if (tech_pvt->channels == 1) { 649 | speex_resampler_process_int( 650 | tech_pvt->resampler, 651 | 0, 652 | (const spx_int16_t *)frame.data, 653 | &in_len, 654 | outbuf, 655 | &out_len 656 | ); 657 | } else { 658 | speex_resampler_process_interleaved_int( 659 | tech_pvt->resampler, 660 | (const spx_int16_t *)frame.data, 661 | &in_len, 662 | outbuf, 663 | &out_len 664 | ); 665 | } 666 | 667 | size_t bytes_written = out_len * tech_pvt->channels * sizeof(spx_int16_t); 668 | if (bytes_written > 0) { 669 | switch_buffer_write( 670 | tech_pvt->sbuffer, 671 | reinterpret_cast(outbuf), 672 | bytes_written 673 | ); 674 | if (switch_buffer_freespace(tech_pvt->sbuffer) == 0) { 675 | flush_sbuffer(); 676 | } 677 | } 678 | } 679 | 680 | flush_sbuffer(); 681 | 682 | switch_mutex_unlock(tech_pvt->mutex); 683 | return SWITCH_TRUE; 684 | } 685 | 686 | switch_status_t stream_session_cleanup(switch_core_session_t *session, char* text, int channelIsClosing) { 687 | switch_channel_t *channel = switch_core_session_get_channel(session); 688 | auto *bug = (switch_media_bug_t*) switch_channel_get_private(channel, MY_BUG_NAME); 689 | if(bug) 690 | { 691 | auto* tech_pvt = (private_t*) switch_core_media_bug_get_user_data(bug); 692 | char sessionId[MAX_SESSION_ID]; 693 | strcpy(sessionId, tech_pvt->sessionId); 694 | 695 | switch_mutex_lock(tech_pvt->mutex); 696 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "(%s) stream_session_cleanup\n", sessionId); 697 | 698 | switch_channel_set_private(channel, MY_BUG_NAME, nullptr); 699 | if (!channelIsClosing) { 700 | switch_core_media_bug_remove(session, &bug); 701 | } 702 | 703 | auto* audioStreamer = (AudioStreamer *) tech_pvt->pAudioStreamer; 704 | if(audioStreamer) { 705 | audioStreamer->deleteFiles(); 706 | if (text) audioStreamer->writeText(text); 707 | finish(tech_pvt); 708 | } 709 | 710 | destroy_tech_pvt(tech_pvt); 711 | 712 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "(%s) stream_session_cleanup: connection closed\n", sessionId); 713 | return SWITCH_STATUS_SUCCESS; 714 | } 715 | 716 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "stream_session_cleanup: no bug - websocket connection already closed\n"); 717 | return SWITCH_STATUS_FALSE; 718 | } 719 | } 720 | 721 | -------------------------------------------------------------------------------- /audio_streamer_glue.h: -------------------------------------------------------------------------------- 1 | #ifndef AUDIO_STREAMER_GLUE_H 2 | #define AUDIO_STREAMER_GLUE_H 3 | #include "mod_audio_stream.h" 4 | 5 | 6 | 7 | int validate_ws_uri(const char* url, char *wsUri); 8 | switch_status_t is_valid_utf8(const char *str); 9 | switch_status_t stream_session_send_text(switch_core_session_t *session, char* text); 10 | switch_status_t stream_session_pauseresume(switch_core_session_t *session, int pause); 11 | switch_status_t stream_session_init(switch_core_session_t *session, responseHandler_t responseHandler, 12 | uint32_t samples_per_second, char *wsUri, int sampling, int channels, char* metadata, void **ppUserData); 13 | switch_bool_t stream_frame(switch_media_bug_t *bug); 14 | switch_status_t stream_session_cleanup(switch_core_session_t *session, char* text, int channelIsClosing); 15 | 16 | #endif //AUDIO_STREAMER_GLUE_H 17 | -------------------------------------------------------------------------------- /base64.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | base64.cpp and base64.h 3 | 4 | base64 encoding and decoding with C++. 5 | More information at 6 | https://renenyffenegger.ch/notes/development/Base64/Encoding-and-decoding-base-64-with-cpp 7 | 8 | Version: 2.rc.09 (release candidate) 9 | 10 | Copyright (C) 2004-2017, 2020-2022 René Nyffenegger 11 | 12 | This source code is provided 'as-is', without any express or implied 13 | warranty. In no event will the author be held liable for any damages 14 | arising from the use of this software. 15 | 16 | Permission is granted to anyone to use this software for any purpose, 17 | including commercial applications, and to alter it and redistribute it 18 | freely, subject to the following restrictions: 19 | 20 | 1. The origin of this source code must not be misrepresented; you must not 21 | claim that you wrote the original source code. If you use this source code 22 | in a product, an acknowledgment in the product documentation would be 23 | appreciated but is not required. 24 | 25 | 2. Altered source versions must be plainly marked as such, and must not be 26 | misrepresented as being the original source code. 27 | 28 | 3. This notice may not be removed or altered from any source distribution. 29 | 30 | René Nyffenegger rene.nyffenegger@adp-gmbh.ch 31 | 32 | */ 33 | 34 | #include "base64.h" 35 | 36 | #include 37 | #include 38 | 39 | // 40 | // Depending on the url parameter in base64_chars, one of 41 | // two sets of base64 characters needs to be chosen. 42 | // They differ in their last two characters. 43 | // 44 | static const char* base64_chars[2] = { 45 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 46 | "abcdefghijklmnopqrstuvwxyz" 47 | "0123456789" 48 | "+/", 49 | 50 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 51 | "abcdefghijklmnopqrstuvwxyz" 52 | "0123456789" 53 | "-_"}; 54 | 55 | static unsigned int pos_of_char(const unsigned char chr) { 56 | // 57 | // Return the position of chr within base64_encode() 58 | // 59 | 60 | if (chr >= 'A' && chr <= 'Z') return chr - 'A'; 61 | else if (chr >= 'a' && chr <= 'z') return chr - 'a' + ('Z' - 'A') + 1; 62 | else if (chr >= '0' && chr <= '9') return chr - '0' + ('Z' - 'A') + ('z' - 'a') + 2; 63 | else if (chr == '+' || chr == '-') return 62; // Be liberal with input and accept both url ('-') and non-url ('+') base 64 characters ( 64 | else if (chr == '/' || chr == '_') return 63; // Ditto for '/' and '_' 65 | else 66 | // 67 | // 2020-10-23: Throw std::exception rather than const char* 68 | //(Pablo Martin-Gomez, https://github.com/Bouska) 69 | // 70 | throw std::runtime_error("Input is not valid base64-encoded data."); 71 | } 72 | 73 | static std::string insert_linebreaks(std::string str, size_t distance) { 74 | // 75 | // Provided by https://github.com/JomaCorpFX, adapted by me. 76 | // 77 | if (!str.length()) { 78 | return ""; 79 | } 80 | 81 | size_t pos = distance; 82 | 83 | while (pos < str.size()) { 84 | str.insert(pos, "\n"); 85 | pos += distance + 1; 86 | } 87 | 88 | return str; 89 | } 90 | 91 | template 92 | static std::string encode_with_line_breaks(String s) { 93 | return insert_linebreaks(base64_encode(s, false), line_length); 94 | } 95 | 96 | template 97 | static std::string encode_pem(String s) { 98 | return encode_with_line_breaks(s); 99 | } 100 | 101 | template 102 | static std::string encode_mime(String s) { 103 | return encode_with_line_breaks(s); 104 | } 105 | 106 | template 107 | static std::string encode(String s, bool url) { 108 | return base64_encode(reinterpret_cast(s.data()), s.length(), url); 109 | } 110 | 111 | std::string base64_encode(unsigned char const* bytes_to_encode, size_t in_len, bool url) { 112 | 113 | size_t len_encoded = (in_len +2) / 3 * 4; 114 | 115 | unsigned char trailing_char = url ? '.' : '='; 116 | 117 | // 118 | // Choose set of base64 characters. They differ 119 | // for the last two positions, depending on the url 120 | // parameter. 121 | // A bool (as is the parameter url) is guaranteed 122 | // to evaluate to either 0 or 1 in C++ therefore, 123 | // the correct character set is chosen by subscripting 124 | // base64_chars with url. 125 | // 126 | const char* base64_chars_ = base64_chars[url]; 127 | 128 | std::string ret; 129 | ret.reserve(len_encoded); 130 | 131 | unsigned int pos = 0; 132 | 133 | while (pos < in_len) { 134 | ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0xfc) >> 2]); 135 | 136 | if (pos+1 < in_len) { 137 | ret.push_back(base64_chars_[((bytes_to_encode[pos + 0] & 0x03) << 4) + ((bytes_to_encode[pos + 1] & 0xf0) >> 4)]); 138 | 139 | if (pos+2 < in_len) { 140 | ret.push_back(base64_chars_[((bytes_to_encode[pos + 1] & 0x0f) << 2) + ((bytes_to_encode[pos + 2] & 0xc0) >> 6)]); 141 | ret.push_back(base64_chars_[ bytes_to_encode[pos + 2] & 0x3f]); 142 | } 143 | else { 144 | ret.push_back(base64_chars_[(bytes_to_encode[pos + 1] & 0x0f) << 2]); 145 | ret.push_back(trailing_char); 146 | } 147 | } 148 | else { 149 | 150 | ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0x03) << 4]); 151 | ret.push_back(trailing_char); 152 | ret.push_back(trailing_char); 153 | } 154 | 155 | pos += 3; 156 | } 157 | 158 | 159 | return ret; 160 | } 161 | 162 | template 163 | static std::string decode(String const& encoded_string, bool remove_linebreaks) { 164 | // 165 | // decode(…) is templated so that it can be used with String = const std::string& 166 | // or std::string_view (requires at least C++17) 167 | // 168 | 169 | if (encoded_string.empty()) return std::string(); 170 | 171 | if (remove_linebreaks) { 172 | 173 | std::string copy(encoded_string); 174 | 175 | copy.erase(std::remove(copy.begin(), copy.end(), '\n'), copy.end()); 176 | 177 | return base64_decode(copy, false); 178 | } 179 | 180 | size_t length_of_string = encoded_string.length(); 181 | size_t pos = 0; 182 | 183 | // 184 | // The approximate length (bytes) of the decoded string might be one or 185 | // two bytes smaller, depending on the amount of trailing equal signs 186 | // in the encoded string. This approximation is needed to reserve 187 | // enough space in the string to be returned. 188 | // 189 | size_t approx_length_of_decoded_string = length_of_string / 4 * 3; 190 | std::string ret; 191 | ret.reserve(approx_length_of_decoded_string); 192 | 193 | while (pos < length_of_string) { 194 | // 195 | // Iterate over encoded input string in chunks. The size of all 196 | // chunks except the last one is 4 bytes. 197 | // 198 | // The last chunk might be padded with equal signs or dots 199 | // in order to make it 4 bytes in size as well, but this 200 | // is not required as per RFC 2045. 201 | // 202 | // All chunks except the last one produce three output bytes. 203 | // 204 | // The last chunk produces at least one and up to three bytes. 205 | // 206 | 207 | size_t pos_of_char_1 = pos_of_char(encoded_string.at(pos+1) ); 208 | 209 | // 210 | // Emit the first output byte that is produced in each chunk: 211 | // 212 | ret.push_back(static_cast( ( (pos_of_char(encoded_string.at(pos+0)) ) << 2 ) + ( (pos_of_char_1 & 0x30 ) >> 4))); 213 | 214 | if ( ( pos + 2 < length_of_string ) && // Check for data that is not padded with equal signs (which is allowed by RFC 2045) 215 | encoded_string.at(pos+2) != '=' && 216 | encoded_string.at(pos+2) != '.' // accept URL-safe base 64 strings, too, so check for '.' also. 217 | ) 218 | { 219 | // 220 | // Emit a chunk's second byte (which might not be produced in the last chunk). 221 | // 222 | unsigned int pos_of_char_2 = pos_of_char(encoded_string.at(pos+2) ); 223 | ret.push_back(static_cast( (( pos_of_char_1 & 0x0f) << 4) + (( pos_of_char_2 & 0x3c) >> 2))); 224 | 225 | if ( ( pos + 3 < length_of_string ) && 226 | encoded_string.at(pos+3) != '=' && 227 | encoded_string.at(pos+3) != '.' 228 | ) 229 | { 230 | // 231 | // Emit a chunk's third byte (which might not be produced in the last chunk). 232 | // 233 | ret.push_back(static_cast( ( (pos_of_char_2 & 0x03 ) << 6 ) + pos_of_char(encoded_string.at(pos+3)) )); 234 | } 235 | } 236 | 237 | pos += 4; 238 | } 239 | 240 | return ret; 241 | } 242 | 243 | std::string base64_decode(std::string const& s, bool remove_linebreaks) { 244 | return decode(s, remove_linebreaks); 245 | } 246 | 247 | std::string base64_encode(std::string const& s, bool url) { 248 | return encode(s, url); 249 | } 250 | 251 | std::string base64_encode_pem (std::string const& s) { 252 | return encode_pem(s); 253 | } 254 | 255 | std::string base64_encode_mime(std::string const& s) { 256 | return encode_mime(s); 257 | } 258 | 259 | #if __cplusplus >= 201703L 260 | // 261 | // Interface with std::string_view rather than const std::string& 262 | // Requires C++17 263 | // Provided by Yannic Bonenberger (https://github.com/Yannic) 264 | // 265 | 266 | std::string base64_encode(std::string_view s, bool url) { 267 | return encode(s, url); 268 | } 269 | 270 | std::string base64_encode_pem(std::string_view s) { 271 | return encode_pem(s); 272 | } 273 | 274 | std::string base64_encode_mime(std::string_view s) { 275 | return encode_mime(s); 276 | } 277 | 278 | std::string base64_decode(std::string_view s, bool remove_linebreaks) { 279 | return decode(s, remove_linebreaks); 280 | } 281 | 282 | #endif // __cplusplus >= 201703L 283 | -------------------------------------------------------------------------------- /base64.h: -------------------------------------------------------------------------------- 1 | // 2 | // base64 encoding and decoding with C++. 3 | // Version: 2.rc.09 (release candidate) 4 | // 5 | 6 | #ifndef BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A 7 | #define BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A 8 | 9 | #include 10 | 11 | #if __cplusplus >= 201703L 12 | #include 13 | #endif // __cplusplus >= 201703L 14 | 15 | std::string base64_encode (std::string const& s, bool url = false); 16 | std::string base64_encode_pem (std::string const& s); 17 | std::string base64_encode_mime(std::string const& s); 18 | 19 | std::string base64_decode(std::string const& s, bool remove_linebreaks = false); 20 | std::string base64_encode(unsigned char const*, size_t len, bool url = false); 21 | 22 | #if __cplusplus >= 201703L 23 | // 24 | // Interface with std::string_view rather than const std::string& 25 | // Requires C++17 26 | // Provided by Yannic Bonenberger (https://github.com/Yannic) 27 | // 28 | std::string base64_encode (std::string_view s, bool url = false); 29 | std::string base64_encode_pem (std::string_view s); 30 | std::string base64_encode_mime(std::string_view s); 31 | 32 | std::string base64_decode(std::string_view s, bool remove_linebreaks = false); 33 | #endif // __cplusplus >= 201703L 34 | 35 | #endif /* BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A */ 36 | -------------------------------------------------------------------------------- /build-mod-audio-stream.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ### Initial one liner: 3 | # sudo apt-get -y install git \ 4 | # && cd /usr/src/ \ 5 | # && git clone https://github.com/amigniter/mod_audio_stream.git \ 6 | # && cd mod_audio_stream \ 7 | # && sudo bash ./build-mod-audio-stream.sh 8 | 9 | apt-get -y install libfreeswitch-dev libssl-dev zlib1g-dev libspeexdsp-dev 10 | 11 | git submodule init 12 | git submodule update 13 | 14 | FS_PKGCONFIG=/usr/local/freeswitch/lib/pkgconfig 15 | if [ -d "$FS_PKGCONFIG" ]; then 16 | export PKG_CONFIG_PATH=$FS_PKGCONFIG 17 | fi 18 | 19 | mkdir build && cd build 20 | cmake -DCMAKE_BUILD_TYPE=Release .. 21 | make 22 | make install 23 | -------------------------------------------------------------------------------- /cmake/FindSpeexDSP.cmake: -------------------------------------------------------------------------------- 1 | # Find the system's SpeexDSP includes and library 2 | # 3 | # SPEEXDSP_INCLUDE_DIRS - where to find SpeexDSP headers 4 | # SPEEXDSP_LIBRARIES - List of libraries when using SpeexDSP 5 | # SPEEXDSP_FOUND - True if SpeexDSP found 6 | # SPEEXDSP_DLL_DIR - (Windows) Path to the SpeexDSP DLL 7 | # SPEEXDSP_DLL - (Windows) Name of the SpeexDSP DLL 8 | 9 | #include(FindWSWinLibs) 10 | #FindWSWinLibs("speexdsp-.*" "SPEEXDSP_HINTS") 11 | 12 | if(NOT USE_REPOSITORY) 13 | find_package(PkgConfig) 14 | pkg_search_module(PC_SPEEXDSP speexdsp) 15 | endif() 16 | 17 | 18 | find_path(SPEEXDSP_INCLUDE_DIR 19 | NAMES 20 | speex/speex_resampler.h 21 | HINTS 22 | ${PC_SPEEXDSP_INCLUDE_DIRS} 23 | ${SPEEXDSP_HINTS}/include 24 | ) 25 | 26 | find_library(SPEEXDSP_LIBRARY 27 | NAMES 28 | speexdsp 29 | HINTS 30 | ${PC_SPEEXDSP_LIBRARY_DIRS} 31 | ${SPEEXDSP_HINTS}/lib 32 | ) 33 | 34 | include(FindPackageHandleStandardArgs) 35 | find_package_handle_standard_args(SpeexDSP DEFAULT_MSG SPEEXDSP_LIBRARY SPEEXDSP_INCLUDE_DIR) 36 | 37 | if(SPEEXDSP_FOUND) 38 | set(SPEEXDSP_LIBRARIES ${SPEEXDSP_LIBRARY}) 39 | set(SPEEXDSP_INCLUDE_DIRS ${SPEEXDSP_INCLUDE_DIR}) 40 | if(WIN32) 41 | set(SPEEXDSP_DLL_DIR "${SPEEXDSP_HINTS}/bin" 42 | CACHE PATH "Path to SpeexDSP DLL" 43 | ) 44 | file(GLOB _speexdsp_dll RELATIVE "${SPEEXDSP_DLL_DIR}" 45 | "${SPEEXDSP_DLL_DIR}/libspeexdsp.dll" 46 | ) 47 | set(SPEEXDSP_DLL ${_speexdsp_dll} 48 | # We're storing filenames only. Should we use STRING instead? 49 | CACHE FILEPATH "SpeexDSP DLL file name" 50 | ) 51 | mark_as_advanced(SPEEXDSP_DLL_DIR SPEEXDSP_DLL) 52 | endif() 53 | else() 54 | set(SPEEXDSP_LIBRARIES) 55 | set(SPEEXDSP_INCLUDE_DIRS) 56 | endif() 57 | 58 | mark_as_advanced(SPEEXDSP_LIBRARIES SPEEXDSP_INCLUDE_DIRS) 59 | -------------------------------------------------------------------------------- /cmake/Packing.cmake: -------------------------------------------------------------------------------- 1 | set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Audio streaming module for FreeSWITCH.") 2 | set(CPACK_PACKAGE_DESCRIPTION "This module allows audio streaming from FreeSWITCH to WebSocket servers, 3 | enabling real-time audio processing and forwarding.") 4 | 5 | set(CPACK_PACKAGE_VENDOR "amsoftswitch") 6 | 7 | set(CPACK_VERBATIM_VARIABLES YES) 8 | 9 | set(CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME}) 10 | SET(CPACK_OUTPUT_FILE_PREFIX "${CMAKE_SOURCE_DIR}/_packages") 11 | 12 | set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) 13 | set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) 14 | set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) 15 | 16 | set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Milan M. ") 17 | set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://github.com/amigniter/mod_audio_stream") 18 | 19 | set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE") 20 | set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md") 21 | 22 | set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) 23 | set(CPACK_COMPONENTS_GROUPING ALL_COMPONENTS_IN_ONE)#ONE_PER_GROUP) 24 | set(CPACK_DEB_COMPONENT_INSTALL YES) 25 | 26 | set(CPACK_STRIP_FILES YES) 27 | 28 | include(CPack) 29 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | mod-audio-stream (1.0.0-2) UNRELEASED; urgency=low 2 | 3 | * Introduced libwsc. 4 | * Improved buffer‐flush logic in streaming path. 5 | 6 | -- Milan M. Sat, 31 May 2025 10:00:00 +0200 7 | 8 | mod-audio-stream (1.0.0-1) UNRELEASED; urgency=low 9 | 10 | * Initial release. 11 | 12 | -- Milan M. Thu, 28 Nov 2024 13:20:00 +0200 -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: mod-audio-stream 3 | Upstream-Contact: Milan M. 4 | Source: https://github.com/amigniter/mod_audio_stream 5 | 6 | Files: * 7 | Copyright: 2024 Milan M. 8 | License: MIT 9 | -------------------------------------------------------------------------------- /mod_audio_stream.c: -------------------------------------------------------------------------------- 1 | /* 2 | * mod_audio_stream FreeSWITCH module to stream audio to websocket and receive response 3 | */ 4 | #include "mod_audio_stream.h" 5 | #include "audio_streamer_glue.h" 6 | 7 | SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_audio_stream_shutdown); 8 | SWITCH_MODULE_RUNTIME_FUNCTION(mod_audio_stream_runtime); 9 | SWITCH_MODULE_LOAD_FUNCTION(mod_audio_stream_load); 10 | 11 | SWITCH_MODULE_DEFINITION(mod_audio_stream, mod_audio_stream_load, mod_audio_stream_shutdown, NULL /*mod_audio_stream_runtime*/); 12 | 13 | static void responseHandler(switch_core_session_t* session, const char* eventName, const char* json) { 14 | switch_event_t *event; 15 | switch_channel_t *channel = switch_core_session_get_channel(session); 16 | switch_event_create_subclass(&event, SWITCH_EVENT_CUSTOM, eventName); 17 | switch_channel_event_set_data(channel, event); 18 | if (json) switch_event_add_body(event, "%s", json); 19 | switch_event_fire(&event); 20 | } 21 | 22 | static switch_bool_t capture_callback(switch_media_bug_t *bug, void *user_data, switch_abc_type_t type) 23 | { 24 | switch_core_session_t *session = switch_core_media_bug_get_session(bug); 25 | private_t *tech_pvt = (private_t *)user_data; 26 | 27 | switch (type) { 28 | case SWITCH_ABC_TYPE_INIT: 29 | break; 30 | 31 | case SWITCH_ABC_TYPE_CLOSE: 32 | { 33 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "Got SWITCH_ABC_TYPE_CLOSE.\n"); 34 | // Check if this is a normal channel closure or a requested closure 35 | int channelIsClosing = tech_pvt->close_requested ? 0 : 1; 36 | stream_session_cleanup(session, NULL, channelIsClosing); 37 | } 38 | break; 39 | 40 | case SWITCH_ABC_TYPE_READ: 41 | if (tech_pvt->close_requested) { 42 | return SWITCH_FALSE; 43 | } 44 | return stream_frame(bug); 45 | break; 46 | 47 | case SWITCH_ABC_TYPE_WRITE: 48 | default: 49 | break; 50 | } 51 | 52 | return SWITCH_TRUE; 53 | } 54 | 55 | static switch_status_t start_capture(switch_core_session_t *session, 56 | switch_media_bug_flag_t flags, 57 | char* wsUri, 58 | int sampling, 59 | char* metadata) 60 | { 61 | switch_channel_t *channel = switch_core_session_get_channel(session); 62 | switch_media_bug_t *bug; 63 | switch_status_t status; 64 | switch_codec_t* read_codec; 65 | 66 | void *pUserData = NULL; 67 | int channels = (flags & SMBF_STEREO) ? 2 : 1; 68 | 69 | if (switch_channel_get_private(channel, MY_BUG_NAME)) { 70 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "mod_audio_stream: bug already attached!\n"); 71 | return SWITCH_STATUS_FALSE; 72 | } 73 | 74 | if (switch_channel_pre_answer(channel) != SWITCH_STATUS_SUCCESS) { 75 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "mod_audio_stream: channel must have reached pre-answer status before calling start!\n"); 76 | return SWITCH_STATUS_FALSE; 77 | } 78 | 79 | read_codec = switch_core_session_get_read_codec(session); 80 | 81 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "calling stream_session_init.\n"); 82 | if (SWITCH_STATUS_FALSE == stream_session_init(session, responseHandler, read_codec->implementation->actual_samples_per_second, 83 | wsUri, sampling, channels, metadata, &pUserData)) { 84 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "Error initializing mod_audio_stream session.\n"); 85 | return SWITCH_STATUS_FALSE; 86 | } 87 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "adding bug.\n"); 88 | if ((status = switch_core_media_bug_add(session, MY_BUG_NAME, NULL, capture_callback, pUserData, 0, flags, &bug)) != SWITCH_STATUS_SUCCESS) { 89 | return status; 90 | } 91 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "setting bug private data.\n"); 92 | switch_channel_set_private(channel, MY_BUG_NAME, bug); 93 | 94 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "exiting start_capture.\n"); 95 | return SWITCH_STATUS_SUCCESS; 96 | } 97 | 98 | static switch_status_t do_stop(switch_core_session_t *session, char* text) 99 | { 100 | switch_status_t status = SWITCH_STATUS_SUCCESS; 101 | 102 | if (text) { 103 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "mod_audio_stream: stop w/ final text %s\n", text); 104 | } 105 | else { 106 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "mod_audio_stream: stop\n"); 107 | } 108 | status = stream_session_cleanup(session, text, 0); 109 | 110 | return status; 111 | } 112 | 113 | static switch_status_t do_pauseresume(switch_core_session_t *session, int pause) 114 | { 115 | switch_status_t status = SWITCH_STATUS_SUCCESS; 116 | 117 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "mod_audio_stream: %s\n", pause ? "pause" : "resume"); 118 | status = stream_session_pauseresume(session, pause); 119 | 120 | return status; 121 | } 122 | 123 | static switch_status_t send_text(switch_core_session_t *session, char* text) { 124 | switch_status_t status = SWITCH_STATUS_FALSE; 125 | switch_channel_t *channel = switch_core_session_get_channel(session); 126 | switch_media_bug_t *bug = switch_channel_get_private(channel, MY_BUG_NAME); 127 | 128 | if (bug) { 129 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "mod_audio_stream: sending text: %s.\n", text); 130 | status = stream_session_send_text(session, text); 131 | } 132 | else { 133 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "mod_audio_stream: no bug, failed sending text: %s.\n", text); 134 | } 135 | return status; 136 | } 137 | 138 | #define STREAM_API_SYNTAX " [start | stop | send_text | pause | resume | graceful-shutdown ] [wss-url | path] [mono | mixed | stereo] [8000 | 16000] [metadata]" 139 | SWITCH_STANDARD_API(stream_function) 140 | { 141 | char *mycmd = NULL, *argv[6] = { 0 }; 142 | int argc = 0; 143 | 144 | switch_status_t status = SWITCH_STATUS_FALSE; 145 | 146 | if (!zstr(cmd) && (mycmd = strdup(cmd))) { 147 | argc = switch_separate_string(mycmd, ' ', argv, (sizeof(argv) / sizeof(argv[0]))); 148 | } 149 | assert(cmd); 150 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "mod_audio_stream cmd: %s\n", cmd ? cmd : ""); 151 | 152 | if (zstr(cmd) || argc < 2 || (0 == strcmp(argv[1], "start") && argc < 4)) { 153 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "Error with command %s %s %s.\n", cmd, argv[0], argv[1]); 154 | stream->write_function(stream, "-USAGE: %s\n", STREAM_API_SYNTAX); 155 | goto done; 156 | } else { 157 | switch_core_session_t *lsession = NULL; 158 | if ((lsession = switch_core_session_locate(argv[0]))) { 159 | if (!strcasecmp(argv[1], "stop")) { 160 | if(argc > 2 && (is_valid_utf8(argv[2]) != SWITCH_STATUS_SUCCESS)) { 161 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, 162 | "%s contains invalid utf8 characters\n", argv[2]); 163 | switch_core_session_rwunlock(lsession); 164 | goto done; 165 | } 166 | status = do_stop(lsession, argc > 2 ? argv[2] : NULL); 167 | } else if (!strcasecmp(argv[1], "pause")) { 168 | status = do_pauseresume(lsession, 1); 169 | } else if (!strcasecmp(argv[1], "resume")) { 170 | status = do_pauseresume(lsession, 0); 171 | } else if (!strcasecmp(argv[1], "send_text")) { 172 | if (argc < 3) { 173 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, 174 | "send_text requires an argument specifying text to send\n"); 175 | switch_core_session_rwunlock(lsession); 176 | goto done; 177 | } 178 | if(is_valid_utf8(argv[2]) != SWITCH_STATUS_SUCCESS) { 179 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, 180 | "%s contains invalid utf8 characters\n", argv[2]); 181 | switch_core_session_rwunlock(lsession); 182 | goto done; 183 | } 184 | status = send_text(lsession, argv[2]); 185 | } else if (!strcasecmp(argv[1], "start")) { 186 | //switch_channel_t *channel = switch_core_session_get_channel(lsession); 187 | char wsUri[MAX_WS_URI]; 188 | int sampling = 8000; 189 | switch_media_bug_flag_t flags = SMBF_READ_STREAM; 190 | char *metadata = argc > 5 ? argv[5] : NULL; 191 | if(metadata && (is_valid_utf8(argv[2]) != SWITCH_STATUS_SUCCESS)) { 192 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, 193 | "%s contains invalid utf8 characters\n", argv[2]); 194 | switch_core_session_rwunlock(lsession); 195 | goto done; 196 | } 197 | if (0 == strcmp(argv[3], "mixed")) { 198 | flags |= SMBF_WRITE_STREAM; 199 | } else if (0 == strcmp(argv[3], "stereo")) { 200 | flags |= SMBF_WRITE_STREAM; 201 | flags |= SMBF_STEREO; 202 | } else if (0 != strcmp(argv[3], "mono")) { 203 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, 204 | "invalid mix type: %s, must be mono, mixed, or stereo\n", argv[3]); 205 | switch_core_session_rwunlock(lsession); 206 | goto done; 207 | } 208 | if (argc > 4) { 209 | if (0 == strcmp(argv[4], "16k")) { 210 | sampling = 16000; 211 | } else if (0 == strcmp(argv[4], "8k")) { 212 | sampling = 8000; 213 | } else { 214 | sampling = atoi(argv[4]); 215 | } 216 | } 217 | if (!validate_ws_uri(argv[2], &wsUri[0])) { 218 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, 219 | "invalid websocket uri: %s\n", argv[2]); 220 | } else if (sampling % 8000 != 0) { 221 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, 222 | "invalid sample rate: %s\n", argv[4]); 223 | } else { 224 | status = start_capture(lsession, flags, wsUri, sampling, metadata); 225 | } 226 | } else { 227 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, 228 | "unsupported mod_audio_stream cmd: %s\n", argv[1]); 229 | } 230 | switch_core_session_rwunlock(lsession); 231 | } else { 232 | switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "Error locating session %s\n", 233 | argv[0]); 234 | } 235 | } 236 | 237 | if (status == SWITCH_STATUS_SUCCESS) { 238 | stream->write_function(stream, "+OK Success\n"); 239 | } else { 240 | stream->write_function(stream, "-ERR Operation Failed\n"); 241 | } 242 | 243 | done: 244 | switch_safe_free(mycmd); 245 | return SWITCH_STATUS_SUCCESS; 246 | } 247 | 248 | SWITCH_MODULE_LOAD_FUNCTION(mod_audio_stream_load) 249 | { 250 | switch_api_interface_t *api_interface; 251 | 252 | switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "mod_audio_stream API loading..\n"); 253 | 254 | /* connect my internal structure to the blank pointer passed to me */ 255 | *module_interface = switch_loadable_module_create_module_interface(pool, modname); 256 | 257 | /* create/register custom event message types */ 258 | if (switch_event_reserve_subclass(EVENT_JSON) != SWITCH_STATUS_SUCCESS || 259 | switch_event_reserve_subclass(EVENT_CONNECT) != SWITCH_STATUS_SUCCESS || 260 | switch_event_reserve_subclass(EVENT_ERROR) != SWITCH_STATUS_SUCCESS || 261 | switch_event_reserve_subclass(EVENT_DISCONNECT) != SWITCH_STATUS_SUCCESS) { 262 | switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't register an event subclass for mod_audio_stream API.\n"); 263 | return SWITCH_STATUS_TERM; 264 | } 265 | SWITCH_ADD_API(api_interface, "uuid_audio_stream", "audio_stream API", stream_function, STREAM_API_SYNTAX); 266 | switch_console_set_complete("add uuid_audio_stream ::console::list_uuid start wss-url metadata"); 267 | switch_console_set_complete("add uuid_audio_stream ::console::list_uuid start wss-url"); 268 | switch_console_set_complete("add uuid_audio_stream ::console::list_uuid stop"); 269 | switch_console_set_complete("add uuid_audio_stream ::console::list_uuid pause"); 270 | switch_console_set_complete("add uuid_audio_stream ::console::list_uuid resume"); 271 | switch_console_set_complete("add uuid_audio_stream ::console::list_uuid send_text"); 272 | 273 | switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "mod_audio_stream API successfully loaded\n"); 274 | 275 | /* indicate that the module should continue to be loaded */ 276 | return SWITCH_STATUS_SUCCESS; 277 | } 278 | 279 | /* 280 | Called when the system shuts down 281 | Macro expands to: switch_status_t mod_audio_stream_shutdown() */ 282 | SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_audio_stream_shutdown) 283 | { 284 | switch_event_free_subclass(EVENT_JSON); 285 | switch_event_free_subclass(EVENT_CONNECT); 286 | switch_event_free_subclass(EVENT_DISCONNECT); 287 | switch_event_free_subclass(EVENT_ERROR); 288 | 289 | return SWITCH_STATUS_SUCCESS; 290 | } 291 | -------------------------------------------------------------------------------- /mod_audio_stream.h: -------------------------------------------------------------------------------- 1 | #ifndef MOD_AUDIO_STREAM_H 2 | #define MOD_AUDIO_STREAM_H 3 | 4 | #include 5 | #include 6 | 7 | #define MY_BUG_NAME "audio_stream" 8 | #define MAX_SESSION_ID (256) 9 | #define MAX_WS_URI (4096) 10 | #define MAX_METADATA_LEN (8192) 11 | 12 | #define EVENT_CONNECT "mod_audio_stream::connect" 13 | #define EVENT_DISCONNECT "mod_audio_stream::disconnect" 14 | #define EVENT_ERROR "mod_audio_stream::error" 15 | #define EVENT_JSON "mod_audio_stream::json" 16 | #define EVENT_PLAY "mod_audio_stream::play" 17 | 18 | typedef void (*responseHandler_t)(switch_core_session_t* session, const char* eventName, const char* json); 19 | 20 | struct private_data { 21 | switch_mutex_t *mutex; 22 | char sessionId[MAX_SESSION_ID]; 23 | SpeexResamplerState *resampler; 24 | responseHandler_t responseHandler; 25 | void *pAudioStreamer; 26 | char ws_uri[MAX_WS_URI]; 27 | int sampling; 28 | int channels; 29 | int audio_paused:1; 30 | int close_requested:1; 31 | char initialMetadata[8192]; 32 | switch_buffer_t *sbuffer; 33 | int rtp_packets; 34 | }; 35 | 36 | typedef struct private_data private_t; 37 | 38 | enum notifyEvent_t { 39 | CONNECT_SUCCESS, 40 | CONNECT_ERROR, 41 | CONNECTION_DROPPED, 42 | MESSAGE 43 | }; 44 | 45 | #endif //MOD_AUDIO_STREAM_H 46 | --------------------------------------------------------------------------------