├── .gitignore ├── src ├── VoiceChannelData.h ├── log.h ├── request_id.h ├── example.cpp ├── WorkQueue.h ├── simpleListeners.h ├── RemotePlanBSdp.h ├── WebSocketTransport.h ├── VoiceChannel.h ├── sdpUtils.h └── Handler.h ├── .editorconfig ├── CMakeLists.txt ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | cmake-build-debug 2 | .idea 3 | build 4 | -------------------------------------------------------------------------------- /src/VoiceChannelData.h: -------------------------------------------------------------------------------- 1 | #ifndef _VoiceChannelData_h_ 2 | #define _VoiceChannelData_h_ 3 | 4 | #include "json.hpp" 5 | 6 | using json = nlohmann::json; 7 | 8 | using ConsumerInfo = json; 9 | using RoomSettings = json; 10 | 11 | #endif //_VoiceChannelData_h_ 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig file: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | indent_style = space 9 | indent_size = 2 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /src/log.h: -------------------------------------------------------------------------------- 1 | #ifndef API_LOG_H_ 2 | #define API_LOG_H_ 3 | 4 | #include 5 | #include 6 | 7 | void log(std::string message) { 8 | std::cout << "[" << std::this_thread::get_id() << "] " << message << std::endl; 9 | } 10 | 11 | void logError(std::string message) { 12 | std::cerr << "\033[1;31m[" << std::this_thread::get_id() << "]\033[0m " << message << std::endl; 13 | } 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /src/request_id.h: -------------------------------------------------------------------------------- 1 | #ifndef _request_id_h_ 2 | #define _request_id_h_ 3 | 4 | #include 5 | #include 6 | 7 | // Our random number generator 8 | boost::random::random_device rng; 9 | boost::random::uniform_int_distribution<> request_id_dist(1000000, 9999999); 10 | 11 | int make_request_id () { 12 | // log("Generating request id..."); 13 | int request_id = request_id_dist(rng); 14 | // log("Request id is generated! " + std::to_string(request_id)); 15 | return request_id; 16 | } 17 | 18 | int randomNumber () { 19 | return make_request_id(); 20 | } 21 | #endif 22 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(mediasoup-client-native-cpp) 2 | cmake_minimum_required (VERSION 3.3) 3 | 4 | find_package(libwebrtc REQUIRED) 5 | find_package(openssl REQUIRED) 6 | find_package(boost REQUIRED) 7 | set(CMAKE_CXX_STANDARD 14) 8 | 9 | include_directories(src) 10 | 11 | add_executable(example src/example.cpp src/log.h src/request_id.h src/json.hpp src/VoiceChannel.h src/Handler.h src/WebSocketTransport.h src/VoiceChannelData.h src/simpleListeners.h src/sdpUtils.h src/RemotePlanBSdp.h src/WorkQueue.h) 12 | 13 | target_link_libraries(example ${WEBRTC_LIBRARIES} ${OPENSSL_LIBRARIES} sdptransform) 14 | 15 | set_target_properties(example PROPERTIES LINK_FLAGS "-lboost_system -lboost_random") 16 | -------------------------------------------------------------------------------- /src/example.cpp: -------------------------------------------------------------------------------- 1 | #include "VoiceChannel.h" 2 | #include "log.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "WebSocketTransport.h" 11 | #include "Handler.h" 12 | #include "request_id.h" 13 | 14 | int main() { 15 | log("Example App"); 16 | boost::asio::io_context ioc; 17 | auto username = "Kim-" + std::to_string(randomNumber()); 18 | auto vc = std::make_shared( 19 | ioc, 20 | "localhost", "3443", 21 | "testroom", 22 | username); 23 | vc->joinRoom(); 24 | 25 | while (true) { 26 | ioc.poll(); 27 | rtc::Thread::Current()->ProcessMessages(0); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Warning: This code is just experimental stuff, you probably won't get it to work. 2 | 3 | # MediaSoup - native client 4 | 5 | This project is a native C++ client for MediaSoup. 6 | It is still a work in progress. 7 | 8 | ## Dependencies 9 | 10 | 1) You need libwebrtc, you can download an installer here: https://drive.google.com/drive/folders/0B398g_p42xgrbUF3VjlFNnNxb3M 11 | 2) Boost, version 1.66 or higher is needed. 12 | 3) OpenSSL 13 | 4) libsdptransform, install directions are here: https://github.com/ibc/libsdptransform 14 | 15 | ## Instructions for MacOS build 16 | 17 | Requirements: Install CMake, Xcode, and the dependencies listed above. 18 | 19 | To build: `cd` into the project; `mkdir build`, `cd build`, `cmake ..`, and then `make`. 20 | 21 | You should get an executable file in build/example. 22 | -------------------------------------------------------------------------------- /src/WorkQueue.h: -------------------------------------------------------------------------------- 1 | #ifndef _WorkQueue_h_ 2 | #define _WorkQueue_h_ 3 | 4 | #include 5 | #include 6 | 7 | #include "log.h" 8 | 9 | class WorkQueue { 10 | std::queue)>> elements; 11 | 12 | public: 13 | void run (std::function)> func) { 14 | log(">=>=>=>=> Adding task to the work queue!"); 15 | elements.push(func); 16 | taskLoop(); 17 | } 18 | 19 | void taskLoop () { 20 | if (!elements.empty()) { 21 | auto workFunction = elements.front(); 22 | elements.pop(); 23 | log(">=>=>=>=> Now running a task in the queue!"); 24 | workFunction([=]() { 25 | log(">=>=>=>=> Task done!!"); 26 | taskLoop(); 27 | }); 28 | } 29 | } 30 | }; 31 | 32 | #endif //_WorkQueue_h_ 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 project contributors 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 | -------------------------------------------------------------------------------- /src/simpleListeners.h: -------------------------------------------------------------------------------- 1 | #ifndef _SimpleSetSessionDescriptionListener_h_ 2 | #define _SimpleSetSessionDescriptionListener_h_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include "log.h" 8 | 9 | class SimpleCreateSessionDescriptionObserver : public webrtc::CreateSessionDescriptionObserver { 10 | private: 11 | std::string handlerName; 12 | std::function onSuccess; 13 | public: 14 | explicit SimpleCreateSessionDescriptionObserver( 15 | std::string handlerName, 16 | std::function onSuccess 17 | ): 18 | handlerName(handlerName), 19 | onSuccess(onSuccess) {} 20 | 21 | // CreateSessionDescriptionObserver implementation. 22 | void OnSuccess(webrtc::SessionDescriptionInterface* desc) override { 23 | onSuccess(desc); 24 | }; 25 | 26 | void OnFailure(const std::string& error) override { 27 | logError("Failed to create SDP [" + handlerName + "]: " + error); 28 | }; 29 | }; 30 | 31 | class SimpleSetSessionDescriptionObserver : public webrtc::SetSessionDescriptionObserver { 32 | private: 33 | std::string handlerName; 34 | std::function onSuccess; 35 | 36 | public: 37 | explicit SimpleSetSessionDescriptionObserver( 38 | std::string handlerName, 39 | std::function onSuccess 40 | ): 41 | handlerName(handlerName), 42 | onSuccess(onSuccess) {} 43 | void OnSuccess() override { 44 | log("Set SDP success [" + handlerName + "]!"); 45 | onSuccess(); 46 | }; 47 | void OnFailure(const std::string& error) override { 48 | logError("Set SDP failed [" + handlerName + "]: " + error); 49 | }; 50 | }; 51 | 52 | 53 | webrtc::SessionDescriptionInterface* getOfferSync(webrtc::PeerConnectionInterface* connection, string handlerName) { 54 | webrtc::SessionDescriptionInterface* p; 55 | std::promise waiting; 56 | connection->CreateOffer(new rtc::RefCountedObject(handlerName, [&](webrtc::SessionDescriptionInterface* desc) { 57 | p = desc; 58 | }), nullptr); 59 | waiting.get_future().get(); 60 | return p; 61 | } 62 | 63 | 64 | #endif 65 | -------------------------------------------------------------------------------- /src/RemotePlanBSdp.h: -------------------------------------------------------------------------------- 1 | #ifndef _RemotePlanBSdp_h_ 2 | #define _RemotePlanBSdp_h_ 3 | 4 | #include "json.hpp" 5 | #include "request_id.h" 6 | #include "log.h" 7 | 8 | using json = nlohmann::json; 9 | using std::string; 10 | 11 | class RemoteSdp { 12 | protected: 13 | json rtpParametersByKind; 14 | json transportLocalParameters = nullptr; 15 | json transportRemoteParameters = nullptr; 16 | int globalId; 17 | int globalVersion = 0; 18 | 19 | public: 20 | RemoteSdp (json rtpParametersByKind) 21 | : rtpParametersByKind(rtpParametersByKind) 22 | { 23 | globalId = randomNumber(); 24 | } 25 | 26 | void setTransportLocalParameters (json transportLocalParameters) { 27 | this->transportLocalParameters = transportLocalParameters; 28 | } 29 | 30 | void setTransportRemoteParameters (json transportRemoteParameters) { 31 | this->transportRemoteParameters = transportRemoteParameters; 32 | } 33 | }; 34 | 35 | class SendRemoteSdp : RemoteSdp { 36 | public: 37 | SendRemoteSdp (json rtpParametersByKind): RemoteSdp(rtpParametersByKind) { 38 | } 39 | json createAnswerSdp (json localSdpObj) { 40 | if (transportLocalParameters == nullptr) { 41 | logError("No transport local parameters"); 42 | } 43 | if (transportRemoteParameters == nullptr) { 44 | logError("No transport remote parameters"); 45 | } 46 | 47 | auto remoteIceParameters = transportRemoteParameters.at("iceParameters"); 48 | auto remoteIceCandidates = transportRemoteParameters.at("iceCandidates"); 49 | auto remoteDtlsParameters = transportRemoteParameters.at("dtlsParameters"); 50 | 51 | string midsStr = ""; 52 | if (localSdpObj.count("media") > 0) { 53 | bool first = true; 54 | for (auto media : localSdpObj.at("media")) { 55 | if (first) { 56 | first = false; 57 | } else { 58 | midsStr += " "; 59 | } 60 | midsStr += media.at("mid").get(); 61 | } 62 | } 63 | 64 | // Increase our SDP version. 65 | globalVersion++; 66 | 67 | json iceLite = remoteIceParameters.at("iceLite").get() ? "ice-lite" : nullptr; 68 | 69 | auto fingerprints = remoteDtlsParameters.at("fingerprints").get>(); 70 | auto lastFingerprint = fingerprints.back(); 71 | 72 | json sdpObj = { 73 | {"version", 0}, 74 | {"origin", { 75 | {"address", "0.0.0.0"}, 76 | {"ipVer", 4}, 77 | {"netType", "IN"}, 78 | {"sessionId", globalId}, 79 | {"sessionVersion", globalVersion}, 80 | {"username", "mediasoup-client"} 81 | }}, 82 | {"name", "-"}, 83 | {"timing", { 84 | {"start", 0}, 85 | {"stop", 0} 86 | }}, 87 | {"iceLite", iceLite}, 88 | {"msidSemantic", { 89 | {"semantic", "WMS"}, 90 | {"token", "*"} 91 | }}, 92 | {"groups", { 93 | {{"type", {"bundle"}}, {"mids", midsStr}} 94 | }}, 95 | {"media", json::array()}, 96 | {"fingerprint", { 97 | {"type", lastFingerprint.at("algorithm")}, 98 | {"hash", lastFingerprint.at("value")} 99 | }} 100 | }; 101 | 102 | if (localSdpObj.count("media") > 0) { 103 | for (auto &localMediaObj : localSdpObj.at("media")) { 104 | auto kind = localMediaObj.at("type").get(); 105 | auto codecs = rtpParametersByKind.at(kind).at("codecs"); 106 | auto headerExtensions = rtpParametersByKind.at(kind).at("headerExtensions"); 107 | 108 | json remoteMediaObj = { 109 | {"type", kind}, 110 | {"port", 7}, 111 | {"protocol", "RTP/SAVPF"}, 112 | {"connection", { 113 | {"ip", "127.0.0.0"}, 114 | {"version", 4} 115 | }}, 116 | {"mid", localMediaObj.at("mid")}, 117 | {"iceUfrag", remoteIceParameters.at("usernameFragment")}, 118 | {"icePwd", remoteIceParameters.at("password")}, 119 | {"candidates", json::array()}, 120 | {"endOfCandidates", "end-of-candidates"}, 121 | {"iceOptions", "renomination"}, 122 | {"rtp", json::array()}, 123 | {"rtcpFb", json::array()}, 124 | {"fmtp", json::array()} 125 | }; 126 | 127 | for (auto &candidate : remoteIceCandidates) { 128 | json candidateObj = { 129 | {"component", 1}, 130 | {"foundation", candidate.at("foundation")}, 131 | {"ip", candidate.at("ip")}, 132 | {"port", candidate.at("port")}, 133 | {"priority", candidate.at("priority")}, 134 | {"transport", candidate.at("protocol")}, 135 | {"type", candidate.at("type")} 136 | }; 137 | if (candidate.count("tcpType") > 0) { 138 | candidateObj.emplace("tcpType", candidate.at("tcpType")); 139 | } 140 | remoteMediaObj.at("candidates").push_back(candidateObj); 141 | } 142 | 143 | if (remoteDtlsParameters.count("role") > 0) { 144 | string role = remoteDtlsParameters.at("role"); 145 | if (role == "client") { 146 | remoteMediaObj.emplace("setup", "active"); 147 | } else if (role == "server") { 148 | remoteMediaObj.emplace("setup", "passive"); 149 | } 150 | } 151 | 152 | if (remoteDtlsParameters.count("direction") > 0) { 153 | string direction = remoteDtlsParameters.at("direction"); 154 | if (direction == "sendrecv" || direction == "recvonly") { 155 | remoteMediaObj.emplace("direction", "recvonly"); 156 | } else if (direction == "recvonly" || direction == "inactive") { 157 | remoteMediaObj.emplace("setup", "inactive"); 158 | } 159 | } 160 | 161 | // If video, be ready for simulcast. 162 | if (kind == "video") { 163 | remoteMediaObj.emplace("xGoogleFlag", "conference"); 164 | } 165 | 166 | for (auto &codec : codecs) { 167 | json rtp = { 168 | {"payload", codec.at("payloadType")}, 169 | {"codec", codec.at("name")}, 170 | {"rate", codec.at("clockRate")} 171 | }; 172 | 173 | if (codec.at("channels") > 1) { 174 | rtp.emplace("encoding", codec.at("channels")); 175 | } 176 | 177 | remoteMediaObj.at("rtp").push_back(rtp); 178 | 179 | if (codec.count("parameters") > 0) { 180 | json paramFmtp = { 181 | {"payload", codec.at("payloadType")}, 182 | {"config", ""} 183 | }; 184 | } 185 | } 186 | } 187 | } 188 | 189 | return sdpObj; 190 | } 191 | }; 192 | #endif 193 | -------------------------------------------------------------------------------- /src/WebSocketTransport.h: -------------------------------------------------------------------------------- 1 | #ifndef _protoo_WebSocketTransport_h_ 2 | #define _protoo_WebSocketTransport_h_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include "json.hpp" 12 | #include "log.h" 13 | #include "request_id.h" 14 | 15 | using tcp = boost::asio::ip::tcp; 16 | using json = nlohmann::json; 17 | using std::string; 18 | namespace websocket = boost::beast::websocket; 19 | namespace ssl = boost::asio::ssl; 20 | 21 | namespace protoo { 22 | 23 | class WebSocketTransport 24 | : public std::enable_shared_from_this 25 | { 26 | public: 27 | class TransportListener { 28 | public: 29 | ~TransportListener() { 30 | log("~TransportListener"); 31 | } 32 | virtual void onTransportError(string error) = 0; 33 | virtual void onTransportConnected() = 0; 34 | virtual void onTransportClose() = 0; 35 | virtual void onNotification(json notification) = 0; 36 | virtual void handleRequest(json request) = 0; 37 | }; 38 | 39 | private: 40 | string host; 41 | string port; 42 | string path; 43 | 44 | std::shared_ptr listener; 45 | websocket::stream> ws; 46 | tcp::resolver resolver; 47 | boost::beast::multi_buffer buffer; // used to read incoming websocket messages 48 | 49 | std::map> handlers; 50 | 51 | std::queue writes; 52 | bool isWriting = false; 53 | 54 | public: 55 | WebSocketTransport( 56 | boost::asio::io_context &ioc, 57 | ssl::context &ctx, 58 | std::shared_ptr listener, 59 | string host, 60 | string port, 61 | string path 62 | ): 63 | host(host), 64 | port(port), 65 | path(path), 66 | listener(listener), 67 | ws(ioc, ctx), 68 | resolver(ioc) { 69 | } 70 | 71 | void connect() { 72 | resolver.async_resolve( 73 | host, 74 | port, 75 | std::bind( 76 | &WebSocketTransport::onResolve, 77 | shared_from_this(), 78 | std::placeholders::_1, 79 | std::placeholders::_2 80 | )); 81 | } 82 | 83 | void send(json payload) { 84 | writes.push(payload); 85 | if (!isWriting) { 86 | doWrite(); 87 | } 88 | } 89 | 90 | void request(string method, json data, std::function onResponse) { 91 | int requestId = make_request_id(); 92 | json req = { 93 | {"request", true}, 94 | {"id", requestId}, 95 | {"method", method}, 96 | {"data", data} 97 | }; 98 | 99 | handlers.emplace(requestId, onResponse); 100 | send(req); 101 | } 102 | 103 | void close() { 104 | ws.close(websocket::close_code::normal); 105 | } 106 | 107 | private: 108 | void doWrite() { 109 | isWriting = true; 110 | auto payload = writes.front(); 111 | writes.pop(); 112 | auto output = payload.dump(); 113 | // log("Sending message " + output); 114 | ws.async_write(boost::asio::buffer(output), std::bind( 115 | &WebSocketTransport::onWriteDone, 116 | shared_from_this(), 117 | std::placeholders::_1, 118 | std::placeholders::_2 119 | )); 120 | } 121 | void onWriteDone(boost::system::error_code ec, 122 | std::size_t bytes_transferred) { 123 | isWriting = false; 124 | // If there are more elements in the write queue, continue with the next one. 125 | if (!writes.empty()) { 126 | doWrite(); 127 | } 128 | } 129 | 130 | void onResolve( 131 | boost::system::error_code ec, 132 | tcp::resolver::results_type results) { 133 | if(ec) { 134 | auto error = "Could not resolve host address"; 135 | logError(error); 136 | listener->onTransportError(error); 137 | } else { 138 | boost::asio::async_connect( 139 | ws.next_layer().next_layer(), 140 | results.begin(), 141 | results.end(), 142 | std::bind( 143 | &WebSocketTransport::onConnect, 144 | shared_from_this(), 145 | std::placeholders::_1)); 146 | } 147 | } 148 | 149 | void onConnect(boost::system::error_code ec) { 150 | if (ec) { 151 | auto error = "Could not connect to the host"; 152 | logError(error); 153 | listener->onTransportError(error); 154 | } else { 155 | ws.next_layer().async_handshake( 156 | ssl::stream_base::client, 157 | std::bind( 158 | &WebSocketTransport::onSSLHandshake, 159 | shared_from_this(), 160 | std::placeholders::_1)); 161 | } 162 | } 163 | 164 | void onSSLHandshake(boost::system::error_code ec) { 165 | if(ec) { 166 | auto error = "Could not perform SSL handshake"; 167 | logError(error); 168 | listener->onTransportError(error); 169 | } else { 170 | ws.async_handshake_ex( 171 | host, 172 | path, 173 | [](websocket::request_type& m) { 174 | m.insert(boost::beast::http::field::sec_websocket_protocol, "protoo"); 175 | }, 176 | std::bind( 177 | &WebSocketTransport::onHandshake, 178 | shared_from_this(), 179 | std::placeholders::_1)); 180 | } 181 | } 182 | 183 | void onHandshake(boost::system::error_code ec) { 184 | if(ec) { 185 | auto error = "Could not perform WebSocket handshake"; 186 | logError(error); 187 | listener->onTransportError(error); 188 | } else { 189 | listener->onTransportConnected(); 190 | readMessage(); 191 | } 192 | } 193 | 194 | void readMessage() { 195 | // Clear the buffer 196 | buffer.consume(buffer.size()); 197 | 198 | ws.async_read( 199 | buffer, 200 | std::bind( 201 | &WebSocketTransport::onReadMessage, 202 | shared_from_this(), 203 | std::placeholders::_1, 204 | std::placeholders::_2 205 | ) 206 | ); 207 | } 208 | 209 | void onReadMessage( 210 | boost::system::error_code ec, 211 | std::size_t bytes_transferred) { 212 | boost::ignore_unused(bytes_transferred); 213 | 214 | // This indicates that the session was closed 215 | if(ec == websocket::error::closed) { 216 | listener->onTransportClose(); 217 | return; 218 | } 219 | 220 | if (ec) { 221 | logError("ERROR: could not receive message! " + ec.message()); 222 | return; 223 | } 224 | 225 | std::stringstream ss; 226 | ss << boost::beast::buffers(buffer.data()); 227 | 228 | // Clear the buffer 229 | buffer.consume(buffer.size()); 230 | 231 | auto str = ss.str(); 232 | 233 | // log("Message: " + str); 234 | 235 | // Read the next message 236 | readMessage(); 237 | 238 | json parsed; 239 | try { 240 | parsed = json::parse(str); 241 | } catch (json::type_error& e) { 242 | logError("Could not parse JSON: " + str); 243 | throw e; 244 | } 245 | handleMessage(parsed); 246 | } 247 | 248 | void handleMessage(json parsed) { 249 | bool isRequest = parsed.count("request") > 0 && parsed.at("request").get(); 250 | bool isResponse = !isRequest && parsed.count("response") > 0 && parsed.at("response").get(); 251 | if (isRequest) { 252 | listener->handleRequest(parsed); 253 | } else if (isResponse) { 254 | int responseId = parsed.at("id").get(); 255 | auto search = handlers.find(responseId); 256 | if (search != handlers.end()) { 257 | std::function handler = handlers.find(responseId)->second; 258 | handlers.erase(responseId); 259 | handler(parsed); 260 | } else { 261 | logError("No handler found for response " + std::to_string(responseId)); 262 | } 263 | } else { 264 | listener->onNotification(parsed); 265 | } 266 | } 267 | }; 268 | } 269 | #endif 270 | -------------------------------------------------------------------------------- /src/VoiceChannel.h: -------------------------------------------------------------------------------- 1 | #ifndef _VoiceChannel_h_ 2 | #define _VoiceChannel_h_ 3 | 4 | #include 5 | #include 6 | 7 | #include "webrtc/rtc_base/ssladapter.h" 8 | 9 | #include "Handler.h" 10 | #include "WebSocketTransport.h" 11 | #include "json.hpp" 12 | #include "log.h" 13 | #include "VoiceChannelData.h" 14 | #include "request_id.h" 15 | 16 | #include 17 | 18 | using json = nlohmann::json; 19 | namespace ssl = boost::asio::ssl; 20 | 21 | class VoiceChannel 22 | : public protoo::WebSocketTransport::TransportListener, 23 | public std::enable_shared_from_this, 24 | public Handler::HandlerListener 25 | { 26 | 27 | string peerName; 28 | boost::asio::io_context &ioc; 29 | std::shared_ptr transport = nullptr; 30 | rtc::scoped_refptr handler = nullptr; 31 | int sendTransportId; 32 | int receiveTransportId; 33 | 34 | public: 35 | VoiceChannel( 36 | boost::asio::io_context &ioc, 37 | string const host, 38 | string const port, 39 | string const roomId, 40 | string const peerName 41 | ): peerName(peerName), ioc(ioc) { 42 | log("Init SSL"); 43 | // for WebRTC 44 | rtc::InitializeSSL(); 45 | // for Boost 46 | ssl::context ctx{ssl::context::sslv23_client}; 47 | log("SSL ok"); 48 | auto path = "/?peerName=" + peerName + "&roomId=" + roomId; 49 | 50 | // cannot use shared_from_this() because we're in the constructor 51 | auto shared = std::shared_ptr(this); 52 | transport = std::make_shared( 53 | ioc, 54 | ctx, 55 | shared, 56 | host, 57 | port, 58 | path 59 | ); 60 | 61 | // auto pcFactory = webrtc::CreatePeerConnectionFactory(); 62 | 63 | this->handler = new rtc::RefCountedObject(shared_from_this(), transport); 64 | } 65 | 66 | void joinRoom() { 67 | log("Join room"); 68 | this->transport->connect(); 69 | } 70 | 71 | void onTransportError(string error) override { 72 | logError("Transport error: " + error); 73 | } 74 | void onTransportConnected() override { 75 | log("Connected!"); 76 | this->transport->request("mediasoup-request", { 77 | {"method", "queryRoom"}, 78 | {"target", "room"} 79 | }, std::bind(&VoiceChannel::initWebRTC, shared_from_this(), std::placeholders::_1)); 80 | 81 | } 82 | void onTransportClose() override { 83 | log("Transport closed!"); 84 | } 85 | void onNotification(json const notification) override { 86 | log("On notification " + notification.dump()); 87 | } 88 | void handleRequest(json const request) override { 89 | auto requestId = request.at("id").get(); 90 | 91 | string method; 92 | if (request.count("method") > 0) { 93 | method = request.at("method").get(); 94 | } 95 | 96 | string dataMethod; 97 | if (request.count("data") > 0 && request.at("data").count("method") > 0) { 98 | dataMethod = request.at("data").at("method").get(); 99 | } 100 | 101 | if (dataMethod == "newPeer") { 102 | handlePeer(request.at("data")); 103 | respondOK(requestId); 104 | } else if (dataMethod == "newConsumer") { 105 | addConsumer(request.at("data")); 106 | respondOK(requestId); 107 | } else if (dataMethod == "peerClosed") { 108 | log("Peer left: " + request.at("data").at("name").get()); 109 | respondOK(requestId); 110 | } else if (dataMethod == "consumerPreferredProfileSet") { 111 | log("Consumer set preferred profile on server - ignore"); 112 | respondOK(requestId); 113 | } else if (method == "active-speaker") { 114 | string activeSpeaker; 115 | if (request.at("data").count("peerName") > 0 && request.at("data").at("peerName").is_string()) { 116 | activeSpeaker = request.at("data").at("peerName").get(); 117 | } 118 | log("Active speaker: " + activeSpeaker); 119 | respondOK(requestId); 120 | } else { 121 | log("Could not understand the request: " + request.dump()); 122 | this->transport->send({ 123 | {"response", true}, 124 | {"id", requestId}, 125 | {"codeCode", 400}, 126 | {"errorReason", "Could not understand the request"} 127 | }); 128 | } 129 | } 130 | void respondOK(int requestId) { 131 | transport->send({ 132 | {"response", true}, 133 | {"id", requestId}, 134 | {"ok", true} 135 | }); 136 | } 137 | 138 | void initWebRTC(json const roomSettings) { 139 | log("Init WebRTC here! Got room settings: " + roomSettings.at("data").dump()); 140 | handler->roomSettings = roomSettings.at("data"); 141 | handler->initWebRTC(); 142 | } 143 | 144 | void onRtpCapabilities(json const nativeCapabilities) override { 145 | 146 | this->transport->request("mediasoup-request", { 147 | {"appData", { 148 | {"device", { 149 | {"flag", "mediasoup-client-cpp"}, 150 | {"name", "mediasoup-client-cpp"}, 151 | {"version", "1.0"} 152 | }}, 153 | {"displayName", peerName} 154 | }}, 155 | {"method", "join"}, 156 | {"target", "room"}, 157 | {"peerName", peerName}, 158 | {"rtpCapabilities", nativeCapabilities} 159 | }, std::bind(&VoiceChannel::onRoomJoin, shared_from_this(), std::placeholders::_1)); 160 | }; 161 | 162 | void onRoomJoin(json const response) { 163 | // ok, let's assume that we joined the room 164 | bool isOk = response.count("ok") > 0 && response.at("ok").get(); 165 | if (!isOk) { 166 | logError("Could not join room: " + response.dump()); 167 | return; 168 | } 169 | 170 | handler->roomSettings = response.at("data"); 171 | 172 | auto peers = response.at("data").at("peers"); 173 | 174 | log("Room join OK!"); 175 | 176 | this->receiveTransportId = randomNumber(); 177 | this->transport->request("mediasoup-request", { 178 | {"appData", { 179 | {"media", "RECV"} 180 | }}, 181 | {"id", receiveTransportId}, 182 | {"direction", "recv"}, 183 | {"method", "createTransport"}, 184 | {"options", { 185 | {"tcp", false} 186 | }}, 187 | {"target", "peer"}, 188 | }, std::bind(&VoiceChannel::onReceiveTransportCreated, shared_from_this(), peers, std::placeholders::_1)); 189 | } 190 | 191 | void onReceiveTransportCreated(json peers, json response) { 192 | log("----> Receive transport!"); 193 | auto remoteTransportSdp = response.at("data"); 194 | handler->remoteReceiveTransportSdp = remoteTransportSdp; 195 | 196 | for(auto const& peer: peers) { 197 | log("Handle peer " + peer.dump()); 198 | handlePeer(peer); 199 | } 200 | 201 | handler->addProducers(); 202 | 203 | /* 204 | this->sendTransportId = randomNumber(); 205 | handler->sendTransportId = this->sendTransportId; 206 | this->transport->request("mediasoup-request", { 207 | {"appData", { 208 | {"media", "SEND_MIC"} 209 | }}, 210 | {"id", sendTransportId}, 211 | {"direction", "send"}, 212 | {"method", "createTransport"}, 213 | {"options", { 214 | {"tcp", false} 215 | }}, 216 | {"target", "peer"}, 217 | }, std::bind(&VoiceChannel::onSendTransportCreated, shared_from_this(), std::placeholders::_1)); 218 | */ 219 | } 220 | 221 | void onSendTransportCreated(json response) { 222 | log("----> Send transport!"); 223 | log("Send transport created: " + response.dump()); 224 | auto remoteTransportSdp = response.at("data"); 225 | handler->remoteSendTransportSdp = remoteTransportSdp; 226 | } 227 | 228 | void handlePeer(json const peer) { 229 | auto consumers = peer.at("consumers"); 230 | for(auto const& consumer: consumers) { 231 | addConsumer(consumer); 232 | } 233 | } 234 | 235 | void addConsumer(json const consumer) { 236 | handler->addConsumer(consumer); 237 | } 238 | }; 239 | 240 | VoiceChannel* makeVoiceChannelAndJoin () { 241 | boost::asio::io_context ioc; 242 | auto vc = std::make_shared( 243 | ioc, 244 | "localhost", "3480", 245 | // "ENGM_TWR", 246 | "g5loxak4", 247 | "N922MB"); 248 | 249 | // vc->initWebRTC(nullptr); 250 | vc->joinRoom(); 251 | ioc.run(); 252 | return vc.get(); 253 | } 254 | 255 | #endif 256 | -------------------------------------------------------------------------------- /src/sdpUtils.h: -------------------------------------------------------------------------------- 1 | #ifndef _sdpUtils_h_ 2 | #define _sdpUtils_h_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | using std::string; 11 | 12 | namespace ortc { 13 | json getRtpCapabilities(json extendedRtpCapabilities) { 14 | auto codecs = json::array(); 15 | auto headerExtensions = json::array(); 16 | 17 | for (auto capCodec : extendedRtpCapabilities.at("codecs")) { 18 | json codec = { 19 | {"name", capCodec.at("name")}, 20 | {"mimeType", capCodec.at("mimeType")}, 21 | {"kind", capCodec.at("kind")}, 22 | {"clockRate", capCodec.at("clockRate")}, 23 | {"preferredPayloadType", capCodec.at("recvPayloadType")}, 24 | {"rtcpFeedback", capCodec.at("rtcpFeedback")}, 25 | {"parameters", capCodec.at("parameters")} 26 | }; 27 | 28 | if (capCodec.count("channels") > 0) { 29 | codec.emplace("channels", capCodec.at("channels")); 30 | } 31 | 32 | codecs.push_back(codec); 33 | 34 | // Add RTX codec. 35 | if (capCodec.count("recvRtxPayloadType")) { 36 | // log("capCodec: " + capCodec.dump()); 37 | json rtxCapCodec = { 38 | {"name", "rtx"}, 39 | {"mimeType", capCodec.at("kind").get() + "/rtx"}, 40 | {"kind", capCodec.at("kind")}, 41 | {"clockRate", capCodec.at("clockRate")}, 42 | {"preferredPayloadType", capCodec.at("recvRtxPayloadType")}, 43 | {"parameters", { 44 | {"apt", capCodec.at("recvPayloadType")} 45 | }} 46 | }; 47 | 48 | // log("FOUND RTX CODEC, ADD IT"); 49 | codecs.push_back(rtxCapCodec); 50 | } 51 | 52 | // TODO: In the future, we need to add FEC, CN, etc, codecs. 53 | } 54 | 55 | for(auto capExt : extendedRtpCapabilities.at("headerExtensions")) { 56 | json ext = { 57 | {"kind", capExt.at("kind")}, 58 | {"uri", capExt.at("uri")}, 59 | {"preferredId", capExt.at("recvId")} 60 | }; 61 | headerExtensions.push_back(ext); 62 | } 63 | 64 | return { 65 | {"codecs", codecs}, 66 | {"headerExtensions", headerExtensions}, 67 | {"fecMechanisms", extendedRtpCapabilities.at("fecMechanisms")} 68 | }; 69 | } 70 | 71 | bool matchCapCodecs (json aCodec, json bCodec) { 72 | auto aMimeType = boost::algorithm::to_lower_copy(aCodec.at("mimeType").get()); 73 | auto bMimeType = boost::algorithm::to_lower_copy(bCodec.at("mimeType").get()); 74 | if (aMimeType != bMimeType) { 75 | return false; 76 | } 77 | if (aCodec.at("clockRate") != bCodec.at("clockRate")) { 78 | return false; 79 | } 80 | if (aCodec.count("channels") > 0 && aCodec.at("channels") != bCodec.at("channels")) { 81 | return false; 82 | } 83 | 84 | // TODO: Match H264 parameters. 85 | 86 | return true; 87 | } 88 | 89 | bool matchCapHeaderExtensions(json aExt, json bExt) { 90 | if (aExt.count("kind") > 0 && bExt.count("kind") > 0 && aExt.at("kind") != bExt.at("kind")) { 91 | return false; 92 | } 93 | if (aExt.at("uri") != bExt.at("uri")) { 94 | return false; 95 | } 96 | return true; 97 | } 98 | 99 | json reduceRtcpFeedback(json codecA, json codecB) { 100 | auto reducedRtcpFeedback = json::array(); 101 | if (codecA.count("rtcpFeedback")) { 102 | for (auto aFb : codecA.at("rtcpFeedback")) { 103 | if (codecB.count("rtcpFeedback")) { 104 | auto codecBRtcpFeedback = codecB.at("rtcpFeedback").get>(); 105 | auto search = std::find_if(codecBRtcpFeedback.begin(), codecBRtcpFeedback.end(), [&](auto bFb) { 106 | return bFb.at("type") == aFb.at("type") && bFb.at("parameter") == aFb.at("parameter"); 107 | }); 108 | if (search != codecBRtcpFeedback.end()) { 109 | reducedRtcpFeedback.push_back(*search); 110 | } 111 | } 112 | } 113 | } 114 | return reducedRtcpFeedback; 115 | } 116 | 117 | json getExtendedRtpCapabilities(json localCaps, json remoteCaps) { 118 | auto codecs = json::array(); 119 | auto headerExtensions = json::array(); 120 | auto fecMechanisms = json::array(); 121 | 122 | if (remoteCaps.count("codecs") > 0) { 123 | // Match media codecs and keep the order preferred by remoteCaps. 124 | for (auto &remoteCodec : remoteCaps.at("codecs")) { 125 | // TODO: Ignore pseudo-codecs and feature codecs. 126 | if (remoteCodec.at("name") == "rtx") { 127 | // log("Found RTX codec, skip it!"); 128 | continue; 129 | } 130 | 131 | if (localCaps.count("codecs") > 0) { 132 | auto localCapsCodecs = localCaps.at("codecs").get>(); 133 | auto searchMatchingLocalCodec = std::find_if(localCapsCodecs.begin(), localCapsCodecs.end(), [&](auto localCodec) { 134 | return matchCapCodecs(localCodec, remoteCodec); 135 | }); 136 | if (searchMatchingLocalCodec != localCapsCodecs.end()) { 137 | json matchingLocalCodec = *searchMatchingLocalCodec; 138 | json extendedCodec = { 139 | {"name", remoteCodec.at("name")}, 140 | {"mimeType", remoteCodec.at("mimeType")}, 141 | {"kind", remoteCodec.at("kind")}, 142 | {"clockRate", remoteCodec.at("clockRate")}, 143 | {"sendPayloadType", remoteCodec.at("preferredPayloadType")}, 144 | {"sendRtxPayloadType", 102 /*nullptr */}, // TODO this should be nullptr, and the code should figure out 102 by itself! 145 | {"recvPayloadType", remoteCodec.at("preferredPayloadType")}, 146 | {"recvRtxPayloadType", 102 /*nullptr */}, // TODO this should be nullptr, and the code should figure out 102 by itself! 147 | {"rtcpFeedback", reduceRtcpFeedback(matchingLocalCodec, remoteCodec)}, 148 | {"parameters", remoteCodec.at("parameters")} 149 | }; 150 | if (remoteCodec.count("channels") > 0) { 151 | extendedCodec.emplace("channels", remoteCodec.at("channels")); 152 | } 153 | codecs.push_back(extendedCodec); 154 | } 155 | } 156 | } 157 | } 158 | 159 | // Match RTX codecs. 160 | for (auto extendedCodec : codecs) { 161 | /* 162 | log("Match RTX codecs... " + codecs.dump()); 163 | log("LocalCaps " + localCaps.dump()); 164 | log("RemoteCaps " + remoteCaps.dump()); 165 | */ 166 | 167 | auto localCapsCodecs = localCaps.at("codecs").get>(); 168 | auto searchMatchingLocalRtxCodec = std::find_if(localCapsCodecs.begin(), localCapsCodecs.end(), [&](auto localCodec) { 169 | return localCodec.at("name") == "rtx" && localCodec.at("parameters").count("apt") > 0 && localCodec.at("parameters").at("apt") == extendedCodec.at("sendPayloadType"); 170 | }); 171 | 172 | auto remoteCapsCodecs = remoteCaps.at("codecs").get>(); 173 | auto searchMatchingRemoteRtxCodec = std::find_if(remoteCapsCodecs.begin(), remoteCapsCodecs.end(), [&](auto remoteCodec) { 174 | return remoteCodec.at("name") == "rtx" && extendedCodec.at("parameters").is_object() && extendedCodec.at("parameters").count("apt") > 0 && extendedCodec.at("parameters").at("apt") == extendedCodec.at("recvPayloadType"); 175 | }); 176 | 177 | if (searchMatchingLocalRtxCodec != localCapsCodecs.end() && searchMatchingRemoteRtxCodec != remoteCapsCodecs.end()) { 178 | // log("FOUND MATCH SO WE ARE HERE WITH TYPES"); 179 | extendedCodec.emplace("sendRtxPayloadType", (*searchMatchingLocalRtxCodec).at("preferredPayloadType")); 180 | extendedCodec.emplace("recvRtxPayloadType", (*searchMatchingRemoteRtxCodec).at("preferredPayloadType")); 181 | } 182 | } 183 | 184 | // Match header extensions. 185 | for (auto remoteExt : remoteCaps.at("headerExtensions")) { 186 | if (localCaps.count("headerExtensions") > 0) { 187 | auto localCapsHeaderExtensions = localCaps.at("headerExtensions").get>(); 188 | auto matchingLocalExt = std::find_if(localCapsHeaderExtensions.begin(), localCapsHeaderExtensions.end(), [&](auto localExt) { 189 | return matchCapHeaderExtensions(localExt, remoteExt); 190 | }); 191 | 192 | if (matchingLocalExt != localCapsHeaderExtensions.end()) { 193 | json extendedExt = { 194 | {"kind", remoteExt.at("kind")}, 195 | {"uri", remoteExt.at("uri")}, 196 | {"sendId", (*matchingLocalExt).at("preferredId")}, 197 | {"recvId", remoteExt.at("preferredId")} 198 | }; 199 | headerExtensions.push_back(extendedExt); 200 | } 201 | } 202 | } 203 | 204 | return { 205 | {"codecs", codecs}, 206 | {"headerExtensions", headerExtensions}, 207 | {"fecMechanisms", fecMechanisms} 208 | }; 209 | } 210 | } 211 | 212 | namespace commonUtils { 213 | json extractRtpCapabilities(json sdpObj) { 214 | // auto dumped = sdpObj.dump(); 215 | // log("SDP OJB" + dumped); 216 | // Map of RtpCodecParameters indexed by payload type. 217 | std::map codecsMap; 218 | 219 | // Array of RtpHeaderExtensions. 220 | std::vector headerExtensions; 221 | 222 | // Whether a m=audio/video section has been already found. 223 | bool gotAudio = false; 224 | bool gotVideo = false; 225 | 226 | for (auto& m : sdpObj.at("media")) { 227 | auto kind = m.at("type").get(); 228 | if (kind == "audio") { 229 | if (gotAudio) { 230 | continue; 231 | } 232 | gotAudio = true; 233 | 234 | } else if (kind == "video") { 235 | if (gotVideo) { 236 | continue; 237 | } 238 | gotVideo = true; 239 | } else { 240 | continue; 241 | } 242 | 243 | // Get codecs. 244 | for (auto &rtp : m.at("rtp")) { 245 | auto codecName = rtp.at("codec").get(); 246 | json channels = 1; 247 | if (rtp.count("encoding") > 0) { 248 | channels = rtp.at("encoding"); 249 | } 250 | json codec = { 251 | {"name", codecName}, 252 | {"mimeType", kind + "/" + codecName}, 253 | {"kind", kind}, 254 | {"clockRate", rtp.at("rate")}, 255 | {"preferredPayloadType", rtp.at("payload")}, 256 | {"channels", channels}, 257 | {"rtcpFeedback", json::array()}, 258 | {"parameters", json::array()} 259 | }; 260 | 261 | if (kind != "audio") { 262 | codec.erase("channels"); 263 | } 264 | 265 | codecsMap.emplace(rtp.at("payload").get(), codec); 266 | } 267 | 268 | // Get codec parameters. 269 | std::vector fmtps; 270 | if (m.count("fmtp") > 0) { 271 | fmtps = m.at("fmtp").get>(); 272 | } 273 | for (auto &fmtp : fmtps) { 274 | auto parameters = sdptransform::parseFmtpConfig(fmtp.at("config").get()); 275 | auto search = codecsMap.find(fmtp.at("payload").get()); 276 | if (search == codecsMap.end()) { 277 | continue; 278 | } else { 279 | json codec = search->second; 280 | codec.emplace("parameters", parameters); 281 | } 282 | } 283 | 284 | // Get RTCP feedback for each codec. 285 | std::vector rtcpFbs; 286 | if (m.count("rtcpFb") > 0) { 287 | rtcpFbs = m.at("rtcpFb").get>(); 288 | } 289 | for(auto &fb : rtcpFbs) { 290 | // log("dump codecsmap " + json(codecsMap).dump()); 291 | int payload = 0; 292 | if (fb.at("payload").is_string()) { 293 | payload = std::stoi(fb.at("payload").get()); 294 | } else { 295 | payload = fb.at("payload").get(); 296 | } 297 | auto search = codecsMap.find(payload); 298 | if (search == codecsMap.end()) { 299 | continue; 300 | } else { 301 | json codec = search->second; 302 | 303 | json feedback = { 304 | {"type", fb.at("type")} 305 | }; 306 | 307 | if (fb.count("subtype") > 0) { 308 | feedback.emplace("parameter", fb.at("subtype")); 309 | } 310 | 311 | codec.at("rtcpFeedback").push_back(feedback); 312 | } 313 | } 314 | 315 | // Get RTP header extensions. 316 | if (m.count("ext") > 0) { 317 | for (auto &ext : m.at("ext")) { 318 | json headerExtension = { 319 | {"kind", kind}, 320 | {"uri", ext.at("uri")}, 321 | {"preferredId", ext.at("value")} 322 | }; 323 | headerExtensions.push_back(headerExtension); 324 | } 325 | } 326 | } 327 | 328 | std::vector codecs; 329 | 330 | for (auto &codecEntry : codecsMap) 331 | { 332 | codecs.push_back(codecEntry.second); 333 | } 334 | 335 | json rtpCapabilities = { 336 | {"codecs", codecs}, 337 | {"headerExtensions", headerExtensions}, 338 | {"fecMechanisms", json::array()} 339 | }; 340 | 341 | return rtpCapabilities; 342 | } 343 | } 344 | 345 | json getExtendedRtpCapabilities(json sdpObj, json roomCapabilities) { 346 | auto clientCapabilities = commonUtils::extractRtpCapabilities(sdpObj); 347 | return ortc::getExtendedRtpCapabilities( 348 | clientCapabilities, 349 | roomCapabilities.at("rtpCapabilities") 350 | ); 351 | } 352 | 353 | json getEffectiveClientRtpCapabilities(string sdp, json roomCapabilities) { 354 | json sdpObj = sdptransform::parse(sdp); 355 | auto extendedRtpCapabilities = getExtendedRtpCapabilities(sdpObj, roomCapabilities); 356 | return ortc::getRtpCapabilities(extendedRtpCapabilities); 357 | } 358 | 359 | 360 | #endif //_sdpUtils_h_ 361 | -------------------------------------------------------------------------------- /src/Handler.h: -------------------------------------------------------------------------------- 1 | #ifndef _ReceiveHandler_h_ 2 | #define _ReceiveHandler_h_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include "WebSocketTransport.h" 17 | #include "VoiceChannelData.h" 18 | #include "simpleListeners.h" 19 | #include "sdpUtils.h" 20 | #include "RemotePlanBSdp.h" 21 | #include "WorkQueue.h" 22 | #include "log.h" 23 | 24 | #include "webrtc/media/engine/webrtcvideocapturerfactory.h" 25 | #include "webrtc/modules/video_capture/video_capture_factory.h" 26 | 27 | using std::string; 28 | 29 | std::unique_ptr OpenVideoCaptureDevice() { 30 | std::vector device_names; 31 | { 32 | std::unique_ptr info( 33 | webrtc::VideoCaptureFactory::CreateDeviceInfo()); 34 | if (!info) { 35 | return nullptr; 36 | } 37 | int num_devices = info->NumberOfDevices(); 38 | for (int i = 0; i < num_devices; ++i) { 39 | const uint32_t kSize = 256; 40 | char name[kSize] = {0}; 41 | char id[kSize] = {0}; 42 | if (info->GetDeviceName(i, name, kSize, id, kSize) != -1) { 43 | device_names.push_back(name); 44 | } 45 | } 46 | } 47 | 48 | cricket::WebRtcVideoDeviceCapturerFactory factory; 49 | std::unique_ptr capturer; 50 | 51 | // This little loop is for debugging 52 | for (const auto& name : device_names) { 53 | log("// Found video device: " + name); 54 | } 55 | 56 | for (const auto& name : device_names) { 57 | capturer = factory.Create(cricket::Device(name, 0)); 58 | if (capturer) { 59 | break; 60 | } 61 | } 62 | return capturer; 63 | } 64 | 65 | class Handler 66 | : public rtc::RefCountInterface 67 | // public webrtc::AudioTrackSinkInterface 68 | { 69 | 70 | class SimplePeerObserver : public webrtc::PeerConnectionObserver, public rtc::RefCountInterface { 71 | private: 72 | string name; 73 | Handler* handler; 74 | public: 75 | SimplePeerObserver(string name, Handler* handler): name(name), handler(handler) {} 76 | 77 | // PeerConnectionObserver implementation. 78 | void OnSignalingChange( 79 | webrtc::PeerConnectionInterface::SignalingState new_state) override { 80 | log(printName() + " OnSignalingChange"); 81 | }; 82 | void OnAddStream( 83 | rtc::scoped_refptr stream) override { 84 | log(printName() + " OnAddStream, " + std::to_string(stream->GetAudioTracks().size()) + " audio tracks, " + std::to_string(stream->GetVideoTracks().size()) + " video tracks"); 85 | for(auto const& audioTrack: stream->GetAudioTracks()) { 86 | log(printName() + " Got an audio track: " + audioTrack->id()); 87 | // handler->addProducer("audio", audioTrack->id()); 88 | /* 89 | handler->addConsumer({ 90 | {"kind", "audio"}, 91 | {"id", audioTrack->id()} 92 | }); 93 | */ 94 | } 95 | for(auto const& videoTrack: stream->GetVideoTracks()) { 96 | log(printName() + " Got a video track: " + videoTrack->id()); 97 | // handler->addProducer("video", videoTrack->id()); 98 | /* 99 | handler->addConsumer({ 100 | {"kind", "video"}, 101 | {"id", videoTrack->id()} 102 | }); 103 | */ 104 | } 105 | }; 106 | void OnRemoveStream( 107 | rtc::scoped_refptr stream) override { 108 | log(printName() + " OnRemoveStream"); 109 | }; 110 | void OnDataChannel( 111 | rtc::scoped_refptr channel) override { 112 | log(printName() + " OnDataChannel"); 113 | } 114 | void OnRenegotiationNeeded() override { 115 | log(printName() + " OnRenegotiationNeeded"); 116 | } 117 | void OnIceConnectionChange( 118 | webrtc::PeerConnectionInterface::IceConnectionState new_state) override { 119 | log(printName() + " OnIceConnectionChange"); 120 | }; 121 | void OnIceGatheringChange( 122 | webrtc::PeerConnectionInterface::IceGatheringState new_state) override { 123 | log(printName() + " OnIceGatheringChange"); 124 | }; 125 | void OnIceCandidate(const webrtc::IceCandidateInterface* candidate) override { 126 | log(printName() + " OnIceCandidate"); 127 | }; 128 | void OnIceConnectionReceivingChange(bool receiving) override { 129 | // log(printName() + " OnIceConnectionReceivingChange: " + std::to_string(receiving)); 130 | } 131 | 132 | string printName() { 133 | return "[" + name + "]"; 134 | } 135 | }; 136 | 137 | public: 138 | class HandlerListener { 139 | public: 140 | virtual void onRtpCapabilities(json nativeRtpCapabilities) = 0; 141 | }; 142 | 143 | RoomSettings roomSettings; 144 | std::map consumers; 145 | json remoteReceiveTransportSdp; 146 | json remoteSendTransportSdp; 147 | int sendTransportId; 148 | int receiveTransportId; 149 | int sendTransportVersion = 0; 150 | int receiveTransportVersion = 0; 151 | 152 | WorkQueue workQueue; 153 | 154 | protected: 155 | std::shared_ptr listener; 156 | std::shared_ptr transport; 157 | rtc::scoped_refptr peerConnectionFactory = nullptr; 158 | rtc::scoped_refptr receivePeerConnection; 159 | rtc::scoped_refptr sendPeerConnection; 160 | rtc::scoped_refptr receiveConnectionListener; 161 | rtc::scoped_refptr sendConnectionListener; 162 | 163 | rtc::scoped_refptr audio_track = nullptr; 164 | rtc::scoped_refptr video_track = nullptr; 165 | 166 | private: 167 | /* 168 | rtc::Thread* worker_thread_ = nullptr; 169 | rtc::Thread* signaling_thread_ = nullptr; 170 | */ 171 | 172 | public: 173 | std::string initialSendOfferSdp; 174 | std::string initialReceiveOfferSdp; 175 | 176 | Handler( 177 | // rtc::scoped_refptr peerConnectionFactory, 178 | std::shared_ptr listener, 179 | std::shared_ptr transport 180 | ): 181 | // peerConnectionFactory(peerConnectionFactory), 182 | listener(listener), 183 | transport(transport) 184 | { 185 | receiveTransportId = randomNumber(); 186 | 187 | /* 188 | auto owned_worker_thread_ = rtc::Thread::Create(); 189 | owned_worker_thread_->Start(); 190 | worker_thread_ = owned_worker_thread_.get(); 191 | 192 | auto owned_signaling_thread_ = rtc::Thread::Create(); 193 | owned_signaling_thread_->Start(); 194 | signaling_thread_ = owned_signaling_thread_.get(); 195 | 196 | peerConnectionFactory = webrtc::CreatePeerConnectionFactory( 197 | worker_thread_, 198 | signaling_thread_, 199 | nullptr, 200 | nullptr, 201 | nullptr, 202 | nullptr 203 | // webrtc::CreateBuiltinAudioEncoderFactory(), 204 | // webrtc::CreateBuiltinAudioDecoderFactory() 205 | ); 206 | */ 207 | peerConnectionFactory = webrtc::CreatePeerConnectionFactory(); 208 | } 209 | ~Handler() { 210 | logError("Destroying the Handler! Much sadness :-("); 211 | } 212 | 213 | void initWebRTC() { 214 | log("makeOffer"); 215 | webrtc::PeerConnectionInterface::RTCConfiguration config; 216 | /* 217 | webrtc::PeerConnectionInterface::IceServer server; 218 | server.uri = "stun:stun.l.google.com:19302"; 219 | config.servers.push_back(server); 220 | */ 221 | 222 | /* This was just a test in mediasoup-client (JS client) 223 | webrtc::PeerConnectionInterface::IceServer server; 224 | server.uri = "turn:worker2.versatica.com:3478?transport=udp"; 225 | server.username = "testuser1"; 226 | server.password = "testpasswd1"; 227 | config.servers.push_back(server); 228 | */ 229 | 230 | webrtc::FakeConstraints constraints; 231 | 232 | constraints.SetMandatoryReceiveAudio(true); 233 | constraints.SetMandatoryReceiveVideo(true); 234 | 235 | config.type = webrtc::PeerConnectionInterface::IceTransportsType::kAll; 236 | config.bundle_policy = webrtc::PeerConnectionInterface::BundlePolicy::kBundlePolicyMaxBundle; 237 | config.rtcp_mux_policy = webrtc::PeerConnectionInterface::RtcpMuxPolicy::kRtcpMuxPolicyRequire; 238 | 239 | sendConnectionListener = new rtc::RefCountedObject("send", this); 240 | receiveConnectionListener = new rtc::RefCountedObject("receive", this); 241 | 242 | receivePeerConnection = peerConnectionFactory->CreatePeerConnection( 243 | config, 244 | &constraints, 245 | nullptr, 246 | nullptr, 247 | sendConnectionListener 248 | ); 249 | 250 | sendPeerConnection = peerConnectionFactory->CreatePeerConnection( 251 | config, 252 | &constraints, 253 | nullptr, 254 | nullptr, 255 | receiveConnectionListener 256 | ); 257 | 258 | audio_track = peerConnectionFactory->CreateAudioTrack( 259 | "audio_label", peerConnectionFactory->CreateAudioSource(nullptr)); 260 | 261 | 262 | auto videoSource = peerConnectionFactory->CreateVideoSource(OpenVideoCaptureDevice(), nullptr); 263 | video_track = peerConnectionFactory->CreateVideoTrack("video_label", videoSource); 264 | 265 | rtc::scoped_refptr stream = 266 | peerConnectionFactory->CreateLocalMediaStream("stream_label"); 267 | 268 | stream->AddTrack(audio_track); 269 | stream->AddTrack(video_track); 270 | if (!sendPeerConnection->AddStream(stream)) { 271 | logError("Adding stream to PeerConnection failed"); 272 | } 273 | 274 | auto sdpListener = new rtc::RefCountedObject("send-createOffer", [&](auto* desc) { 275 | std::string serialized; 276 | desc->ToString(&serialized); 277 | initialSendOfferSdp = serialized; 278 | sendPeerConnection->SetLocalDescription(new rtc::RefCountedObject("send-setLocal", [&](){ 279 | log("Success: set up send peer connection"); 280 | }), desc); 281 | log("Room settings: " + roomSettings.dump()); 282 | auto capabilities = getEffectiveClientRtpCapabilities(serialized, roomSettings); 283 | log("Our capabilities: " + capabilities.dump()); 284 | // listener->onRtpCapabilities(serialized); 285 | listener->onRtpCapabilities(capabilities); 286 | }); 287 | 288 | log("Creating offer..."); 289 | sendPeerConnection->CreateOffer(sdpListener, nullptr); 290 | 291 | /* 292 | receivePeerConnection->CreateOffer(new rtc::RefCountedObject("receive-createOffer", [&](auto* desc) { 293 | std::string serialized; 294 | desc->ToString(&serialized); 295 | initialReceiveOfferSdp = serialized; 296 | receivePeerConnection->SetLocalDescription(new rtc::RefCountedObject("receive-setLocal", [&](){ 297 | log("Success: set up receive peer connection"); 298 | }), desc); 299 | }), nullptr); 300 | */ 301 | log("My work here is done!"); 302 | } 303 | 304 | void addProducers () { 305 | // addProducer("video", video_track->id()); 306 | addProducer("audio", audio_track->id()); 307 | } 308 | 309 | void ensureSendTransportCreated(std::function callback) { 310 | if (remoteSendTransportSdp != nullptr) { 311 | callback(remoteSendTransportSdp); 312 | } else { 313 | sendTransportId = randomNumber(); 314 | transport->request("mediasoup-request", { 315 | {"appData", { 316 | {"media", "SEND"} 317 | }}, 318 | {"id", sendTransportId}, 319 | {"version", sendTransportVersion++}, 320 | {"direction", "send"}, 321 | {"method", "createTransport"}, 322 | {"options", { 323 | {"tcp", false} 324 | }}, 325 | {"target", "peer"}, 326 | }, [=](json response) { 327 | // log("createTransport response: " + response.dump()); 328 | remoteSendTransportSdp = response.at("data"); 329 | callback(remoteSendTransportSdp); 330 | }); 331 | } 332 | } 333 | 334 | void addProducer(std::string kind, std::string id) { 335 | log("Ensure send transport created!"); 336 | ensureSendTransportCreated([=](json desc) { 337 | 338 | log("Now we are sure that it was created"); 339 | //sendPeerConnection->CreateOffer(new rtc::RefCountedObject("send-createOffer", [=](auto* desc) { 340 | std::string serialized; 341 | //desc->ToString(&serialized); 342 | auto sessionDesc = sendPeerConnection->local_description(); 343 | sessionDesc->ToString(&serialized); 344 | // sendPeerConnection->SetLocalDescription(new rtc::RefCountedObject("send-setLocal", [=](){ 345 | log("Created new offer and set it"); 346 | json request = { 347 | {"method", "newProducerSdp"}, 348 | {"kind", kind}, 349 | {"trackId", id}, 350 | {"initialOfferSdp", serialized}, 351 | {"remoteTransportSdp", remoteSendTransportSdp}, 352 | {"transportId", sendTransportId} 353 | }; 354 | transport->request("mediasoup-request", request, std::bind(&Handler::gotProducerData, this, std::placeholders::_1, kind)); 355 | // }), sessionDesc); 356 | //}), nullptr); 357 | }); 358 | } 359 | 360 | void gotProducerData(json data, std::string kind) { 361 | auto remoteSdp = data.at("data").at("sdp").get(); 362 | json rtpParameters = data.at("data").at("rtpParameters"); 363 | 364 | webrtc::SdpParseError error; 365 | 366 | auto remoteAnswer = 367 | webrtc::CreateSessionDescription(webrtc::SessionDescriptionInterface::kAnswer, 368 | remoteSdp, &error); 369 | 370 | sendPeerConnection->SetRemoteDescription(new rtc::RefCountedObject("send-setRemote", [=](){ 371 | 372 | std::string source = "mic"; 373 | if (kind == "video") { 374 | source = "webcam"; 375 | } 376 | json req = { 377 | {"method", "createProducer"}, 378 | {"appData", { 379 | {"source", source} 380 | }}, 381 | {"kind", kind}, 382 | {"paused", false}, 383 | {"target", "peer"}, 384 | {"rtpParameters", rtpParameters}, 385 | {"transportId", sendTransportId} 386 | }; 387 | 388 | transport->request("mediasoup-request", req, [&](json response) { 389 | log("Oh yere, created producer on WS"); 390 | log("Is the video track enabled? " + std::to_string(video_track->enabled())); 391 | if (video_track->state() == webrtc::MediaStreamTrackInterface::TrackState::kLive) { 392 | log("Video track is live :-D"); 393 | } else if (video_track->state() == webrtc::MediaStreamTrackInterface::TrackState::kEnded) { 394 | log("NOOO audio has ended :'("); 395 | } else { 396 | log("WTF audio unknown state..."); 397 | } 398 | 399 | log("Is the audio track enabled? " + std::to_string(audio_track->enabled())); 400 | if (audio_track->state() == webrtc::MediaStreamTrackInterface::TrackState::kLive) { 401 | log("audio track is live :-D"); 402 | } else if (audio_track->state() == webrtc::MediaStreamTrackInterface::TrackState::kEnded) { 403 | log("NOOO audio has ended :'("); 404 | } else { 405 | log("WTF audio unknown state..."); 406 | } 407 | }); 408 | }), remoteAnswer); 409 | } 410 | 411 | // bool alreadyAddedUseForTesting = false; 412 | void addConsumer(ConsumerInfo consumer) { 413 | log(">=>=>=>=> Adding CONSUMER <=<=<=<=<=<=!"); 414 | 415 | workQueue.run([=](std::function cb) { 416 | /* just used as a guard to prevent more consumers, during testing 417 | if (alreadyAddedUseForTesting) { 418 | return; 419 | } 420 | alreadyAddedUseForTesting = true; 421 | */ 422 | 423 | // return; // TODO actually add! 424 | auto consumerId = consumer.at("id").get(); 425 | // log("consumerId=" + std::to_string(consumerId)); 426 | auto kind = consumer.at("kind").get(); 427 | // log("kind=" + kind); 428 | 429 | /* 430 | if (kind != "audio") { 431 | logError("Kind " + kind + " not supported, skipping."); 432 | return; 433 | } 434 | */ 435 | 436 | auto id = consumer.at("id").get(); 437 | // log("id=" + std::to_string(id)); 438 | auto trackId = "consumerZZZZZZ-" + kind + "-" + std::to_string(id); 439 | auto encoding = consumer.at("rtpParameters").at("encodings").at(0); 440 | // log("trackId=" + trackId); 441 | auto ssrc = encoding.at("ssrc").get(); 442 | // log("ssrc=" + std::to_string(ssrc)); 443 | auto cname = consumer.at("rtpParameters").at("rtcp").at("cname").get(); 444 | // log("cname=" + cname); 445 | json consumerInfo = { 446 | {"kind", kind}, 447 | {"trackId", trackId}, 448 | {"ssrc", ssrc}, 449 | {"cname", cname} 450 | }; 451 | 452 | if (encoding.count("rtx") > 0 && encoding.at("rtx").count("ssrc") > 0) { 453 | consumerInfo.emplace("rtxSsrc", encoding.at("rtx").at("ssrc")); 454 | } 455 | 456 | consumers.emplace(consumerId, consumerInfo); 457 | 458 | log("ADDING CONSUMER RIGHT OVER HERE"); 459 | 460 | json request = { 461 | {"method", "newConsumerSdp"}, 462 | {"initialOfferSdp", initialSendOfferSdp}, 463 | // {"initialOfferSdp", initialReceiveOfferSdp}, 464 | {"remoteTransportSdp", remoteReceiveTransportSdp}, 465 | {"transportId", receiveTransportId}, 466 | {"version", receiveTransportVersion++}, 467 | {"consumers", consumers} 468 | }; 469 | transport->request("mediasoup-request", request, std::bind(&Handler::addConsumerWithSdp, this, std::placeholders::_1)); 470 | cb(); 471 | }); 472 | } 473 | 474 | void addConsumerWithSdp(json const sdpResponse) { 475 | auto sdp = sdpResponse.at("data").get(); 476 | 477 | webrtc::SdpParseError error; 478 | 479 | log("DO ADD CONSUMER"); 480 | 481 | auto remoteOffer = 482 | webrtc::CreateSessionDescription(webrtc::SessionDescriptionInterface::kOffer, 483 | sdp, &error); 484 | 485 | if (error.description != "") { 486 | logError("SDP error: " + error.description); 487 | } else { 488 | log("1) Setting remote description"); 489 | receivePeerConnection->SetRemoteDescription(new rtc::RefCountedObject("receive-setRemote", [&](){ 490 | log("2) Remote description set"); 491 | receivePeerConnection->CreateAnswer(new rtc::RefCountedObject("receive-createAnswer", 492 | [&](webrtc::SessionDescriptionInterface* answer){ 493 | log("3) Answer created"); 494 | receivePeerConnection->SetLocalDescription( 495 | new rtc::RefCountedObject("receive-setLocal", [&](){ 496 | log("4) Success: consumer added, SDP descriptions set etc."); 497 | }), 498 | answer); 499 | }), nullptr); 500 | }), remoteOffer); 501 | } 502 | } 503 | }; 504 | 505 | #endif 506 | --------------------------------------------------------------------------------