ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
WebRTCWindowSystem.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
9 
10 #include <Helper.h>
11 #include <IJsonConvertible.h>
12 #include <Logging.h>
13 #include <p2p/base/basic_packet_socket_factory.h>
14 #include <p2p/base/stun_server.h>
15 #include <p2p/base/turn_server.h>
16 #include <rtc_base/ssl_adapter.h>
17 #include <rtc_base/thread.h>
18 
19 #include <chrono>
20 #include <sstream>
21 #include <thread>
22 #include <unordered_map>
23 
24 #include "core/Tensor.h"
30 
31 namespace cloudViewer {
32 namespace visualization {
33 namespace webrtc_server {
34 
35 // List of ICE servers (STUN or TURN). STUN servers is used by default. In
36 // certain network configurations TURN servers are required to forward WebRTC
37 // traffic.
38 static const std::list<std::string> s_public_ice_servers{
39  "stun:stun.l.google.com:19302"};
40 
41 // We also provide an experimental TURN server for testing purposes. Don't abuse
42 // the server. The strings are split to avoid search enging indexing.
43 static const std::list<std::string> s_cloudViewer_ice_servers{
44  std::string("turn:user:password@34.69") + ".27.100:3478",
45  std::string("turn:user:password@34.69") + ".27.100:3478?transport=tcp",
46 };
47 
48 // clang-format off
62 // clang-format on
63 static std::string GetCustomSTUNServer() {
64  if (const char *env_p = std::getenv("WEBRTC_STUN_SERVER")) {
65  return std::string(env_p);
66  } else {
67  return "";
68  }
69 }
70 
71 static std::string GetEnvWebRTCIP() {
72  if (const char *env_p = std::getenv("WEBRTC_IP")) {
73  return std::string(env_p);
74  } else {
75  return "localhost";
76  }
77 }
78 
79 static std::string GetEnvWebRTCPort() {
80  if (const char *env_p = std::getenv("WEBRTC_PORT")) {
81  return std::string(env_p);
82  } else {
83  return "8888";
84  }
85 }
86 
88  std::unordered_map<WebRTCWindowSystem::OSWindow, std::string>
90  std::string GenerateUID() {
91  static std::atomic<size_t> count{0};
92  return "window_" + std::to_string(count++);
93  }
94 
95  // HTTP handshake server settings.
97  std::string http_address_; // Used when http_handshake_enabled_ == true.
98  std::string web_root_; // Used when http_handshake_enabled_ == true.
99 
100  // PeerConnectionManager is used for setting up connections and managing API
101  // call entry points.
102  std::unique_ptr<PeerConnectionManager> peer_connection_manager_ = nullptr;
103 
104  std::thread webrtc_thread_;
105  bool sever_started_ = false;
106 
107  std::unordered_map<std::string, std::function<std::string(std::string)>>
109 };
110 
111 std::shared_ptr<WebRTCWindowSystem> WebRTCWindowSystem::GetInstance() {
112  static std::shared_ptr<WebRTCWindowSystem> instance(new WebRTCWindowSystem);
113  return instance;
114 }
115 
116 WebRTCWindowSystem::WebRTCWindowSystem()
117  : BitmapWindowSystem(
118 #if !defined(__APPLE__) && !defined(_WIN32) && !defined(_WIN64)
119  BitmapWindowSystem::Rendering::HEADLESS
120 #else
121  BitmapWindowSystem::Rendering::NORMAL
122 #endif
123  ),
124  impl_(new WebRTCWindowSystem::Impl()) {
125 
126  // impl_->web_root_ is filled at StartWebRTCServer. It relies on
127  // GetResourcePath(), which happens after Application::Initialize().
128  impl_->http_handshake_enabled_ = true;
129  impl_->http_address_ = GetEnvWebRTCIP() + ":" + GetEnvWebRTCPort();
130 
131  // Server->client send frame.
132  auto draw_callback = [this](const gui::Window *window,
133  std::shared_ptr<core::Tensor> im) -> void {
134  OnFrame(GetWindowUID(window->GetOSWindow()), im);
135  };
136  SetOnWindowDraw(draw_callback);
137 
138  // Client->server message default callbacks.
140  "MouseEvent", [this](const std::string &message) -> std::string {
141  const Json::Value value = utility::StringToJson(message);
142  const std::string window_uid =
143  value.get("window_uid", "").asString();
144  const auto os_window = GetOSWindowByUID(window_uid);
145  if (value.get("class_name", "").asString() == "MouseEvent" &&
146  os_window != nullptr) {
147  gui::MouseEvent me;
148  if (me.FromJson(value)) PostMouseEvent(os_window, me);
149  }
150  return ""; // empty string is not sent back
151  });
152 
153  // Synchronized MouseEvents over multiple windows
155  "SyncMouseEvent",
156  [this](const std::string &message) -> std::string {
157  Json::Value value = utility::StringToJson(message);
158  if (value.get("class_name", "").asString() != "SyncMouseEvent")
159  return "Error.";
160  value["class_name"] = "MouseEvent";
161  gui::MouseEvent me;
162  if (!me.FromJson(value)) return "Bad MouseEvent. Ignoring.";
163  for (const auto &json_window_uid :
164  value.get("window_uid_list", "")) {
165  const auto os_window =
166  GetOSWindowByUID(json_window_uid.asString());
167  if (os_window != nullptr) PostMouseEvent(os_window, me);
168  }
169  return ""; // empty string is not sent back
170  });
171 
173  "ResizeEvent", [this](const std::string &message) -> std::string {
174  const Json::Value value = utility::StringToJson(message);
175  const std::string window_uid =
176  value.get("window_uid", "").asString();
177  const auto os_window = GetOSWindowByUID(window_uid);
178  if (value.get("class_name", "").asString() == "ResizeEvent" &&
179  os_window != nullptr) {
180  const int height = value.get("height", 0).asInt();
181  const int width = value.get("width", 0).asInt();
182  if (height <= 0 || width <= 0) {
183  std::string reply = fmt::format(
184  "Invalid height {} or width {}, ResizeEvent "
185  "ignored.",
186  height, width);
187  utility::LogWarning("{}", reply);
188  return "[CloudViewer WARNING] " + reply;
189  } else {
190  utility::LogDebug("ResizeEvent {}: ({}, {})",
191  window_uid, height, width);
192  SetWindowSize(os_window, width, height);
193  }
194  }
195  return ""; // empty string is not sent back
196  });
197 }
198 
200  impl_->peer_connection_manager_ = nullptr;
201  rtc::Thread::Current()->Quit();
202 }
203 
206  int width,
207  int height,
208  const char *title,
209  int flags) {
210  // No-op if the server is already running.
212  WebRTCWindowSystem::OSWindow os_window = BitmapWindowSystem::CreateOSWindow(
213  o3d_window, width, height, title, flags);
214  std::string window_uid = impl_->GenerateUID();
215  impl_->os_window_to_uid_.insert({os_window, window_uid});
216  utility::LogInfo("Window {} created.", window_uid);
217  return os_window;
218 }
219 
221  std::string window_uid = impl_->os_window_to_uid_.at(w);
222  CloseWindowConnections(window_uid);
223  impl_->os_window_to_uid_.erase(w);
224  BitmapWindowSystem::DestroyWindow(w);
225  utility::LogInfo("Window {} destroyed.", window_uid);
226 }
227 
228 std::vector<std::string> WebRTCWindowSystem::GetWindowUIDs() const {
229  std::vector<std::string> uids;
230  for (const auto &it : impl_->os_window_to_uid_) {
231  uids.push_back(it.second);
232  }
233  return uids;
234 }
235 
238  if (impl_->os_window_to_uid_.count(w) == 0) {
239  return "window_undefined";
240  } else {
241  return impl_->os_window_to_uid_.at(w);
242  }
243 }
244 
246  const std::string &uid) const {
247  // This can be optimized by adding a bi-directional map, but it may not be
248  // worth it since we typically don't have lots of windows.
249  for (const auto &it : impl_->os_window_to_uid_) {
250  if (it.second == uid) {
251  return it.first;
252  };
253  }
254  return nullptr;
255 }
256 
258  if (!impl_->sever_started_) {
259  auto start_webrtc_thread = [this]() {
260  // Ensure Application::Initialize() is called before this.
261  std::string resource_path(
262  gui::Application::GetInstance().GetResourcePath());
263  impl_->web_root_ = resource_path + "/html";
264 
265  // Logging settings.
266  // src/rtc_base/logging.h: LS_VERBOSE, LS_ERROR
267  rtc::LogMessage::LogToDebug((rtc::LoggingSeverity)rtc::LS_ERROR);
268 
269  rtc::LogMessage::LogTimestamps();
270  rtc::LogMessage::LogThreads();
271 
272  // PeerConnectionManager manages all WebRTC connections.
273  rtc::Thread *thread = rtc::Thread::Current();
274  rtc::InitializeSSL();
275  Json::Value config;
276  std::list<std::string> ice_servers;
277  ice_servers.insert(ice_servers.end(), s_public_ice_servers.begin(),
278  s_public_ice_servers.end());
279  if (!GetCustomSTUNServer().empty()) {
280  std::vector<std::string> custom_servers =
282  ice_servers.insert(ice_servers.end(), custom_servers.begin(),
283  custom_servers.end());
284  }
285  ice_servers.insert(ice_servers.end(),
288  utility::LogInfo("ICE servers: {}", ice_servers);
289 
290  impl_->peer_connection_manager_ =
291  std::make_unique<PeerConnectionManager>(
292  ice_servers, config["urls"], ".*", "");
293  if (!impl_->peer_connection_manager_->InitializePeerConnection()) {
294  utility::LogError("InitializePeerConnection() failed.");
295  }
296 
298  "Set WEBRTC_STUN_SERVER environment variable add a "
299  "customized WebRTC STUN server.",
300  impl_->http_address_);
301 
302  // CivetWeb server is used for WebRTC handshake. This is enabled
303  // when running as a standalone application, and is disabled when
304  // running in Jupyter.
305  if (impl_->http_handshake_enabled_) {
306  utility::LogInfo("WebRTC HTTP server handshake mode enabled.");
307  std::vector<std::string> options{"document_root",
308  impl_->web_root_,
309  "enable_directory_listing",
310  "no",
311  "additional_header",
312  "X-Frame-Options: SAMEORIGIN",
313  "access_control_allow_origin",
314  "*",
315  "listening_ports",
316  impl_->http_address_,
317  "enable_keep_alive",
318  "yes",
319  "keep_alive_timeout_ms",
320  "1000",
321  "decode_url",
322  "no"};
323  try {
324  // PeerConnectionManager provides callbacks for the Civet
325  // server.
326  std::map<std::string,
328  func = impl_->peer_connection_manager_
329  ->GetHttpApi();
330 
331  // Main loop for Civet server.
333  "CloudViewer WebVisualizer is serving at "
334  "http://{}.",
335  impl_->http_address_);
337  "Set WEBRTC_IP and WEBRTC_PORT environment "
338  "variable to customize the HTTP server address.",
339  impl_->http_address_);
340  HttpServerRequestHandler civet_server(func, options);
341  thread->Run();
342  } catch (const CivetException &ex) {
343  utility::LogError("Cannot start Civet server: {}",
344  ex.what());
345  }
346  } else {
347  utility::LogInfo("WebRTC Jupyter handshake mode enabled.");
348  thread->Run();
349  }
350  rtc::CleanupSSL();
351  };
352  impl_->webrtc_thread_ = std::thread(start_webrtc_thread);
353  impl_->sever_started_ = true;
354  }
355 }
356 
358  const std::string &message) {
359  utility::LogDebug("WebRTCWindowSystem::OnDataChannelMessage: {}", message);
360  std::string reply("");
361  try {
362  const Json::Value value = utility::StringToJson(message);
363  const std::string class_name = value.get("class_name", "").asString();
364  const std::string window_uid = value.get("window_uid", "").asString();
365 
366  if (impl_->data_channel_message_callbacks_.count(class_name) != 0) {
367  reply = impl_->data_channel_message_callbacks_.at(class_name)(
368  message);
369  const auto os_window = GetOSWindowByUID(window_uid);
370  if (os_window) PostRedrawEvent(os_window);
371  return reply;
372  } else {
373  reply = fmt::format(
374  "OnDataChannelMessage: {}. Message cannot be parsed, as "
375  "the class_name {} is invalid.",
376  message, class_name);
377  }
378  } catch (std::exception &e) { // known error
379  reply = fmt::format(
380  "OnDataChannelMessage: {}. Error processing message: {}",
381  message, e.what());
382  } catch (...) { // unknown error
383  reply = fmt::format(
384  "OnDataChannelMessage: {}. Message cannot be parsed, or "
385  "the target GUI event failed to execute.",
386  message);
387  }
388  utility::LogInfo("{}", reply);
389  return "[CloudViewer WARNING] " +
390  reply; // Add tag for detecting error in client
391 }
392 
394  const std::string &class_name,
395  const std::function<std::string(const std::string &)> callback) {
397  "WebRTCWindowSystem::RegisterDataChannelMessageCallback: {}",
398  class_name);
399  impl_->data_channel_message_callbacks_[class_name] = callback;
400 }
401 
402 void WebRTCWindowSystem::OnFrame(const std::string &window_uid,
403  const std::shared_ptr<core::Tensor> &im) {
404  impl_->peer_connection_manager_->OnFrame(window_uid, im);
405 }
406 
407 void WebRTCWindowSystem::SendInitFrames(const std::string &window_uid) {
408  utility::LogInfo("Sending init frames to {}.", window_uid);
409  static const int s_max_initial_frames = 5;
410  static const int s_sleep_between_frames_ms = 100;
411  const auto os_window = GetOSWindowByUID(window_uid);
412  if (!os_window) return;
413  for (int i = 0; os_window != nullptr && i < s_max_initial_frames; ++i) {
414  PostRedrawEvent(os_window);
415  std::this_thread::sleep_for(
416  std::chrono::milliseconds(s_sleep_between_frames_ms));
417  utility::LogDebug("Sent init frames #{} to {}.", i, window_uid);
418  }
419 }
420 
421 std::string WebRTCWindowSystem::CallHttpAPI(const std::string &entry_point,
422  const std::string &query_string,
423  const std::string &data) const {
424  utility::LogDebug("[Called HTTP API (custom handshake)] {}", entry_point);
425 
426  std::string query_string_trimmed = "";
427  if (!query_string.empty() && query_string[0] == '?') {
428  query_string_trimmed =
429  query_string.substr(1, query_string.length() - 1);
430  }
431  utility::LogDebug("entry_point: {}", entry_point);
432  utility::LogDebug("query_string_trimmed: {}", query_string_trimmed);
433  utility::LogDebug("data: {}", data);
434 
435  std::string result = "";
436  if (entry_point == "/api/getMediaList") {
438  impl_->peer_connection_manager_->GetMediaList());
439  } else if (entry_point == "/api/getIceServers") {
441  impl_->peer_connection_manager_->GetIceServers());
442  } else if (entry_point == "/api/getIceCandidate") {
443  std::string peerid;
444  if (!query_string_trimmed.empty()) {
445  CivetServer::getParam(query_string_trimmed.c_str(), "peerid",
446  peerid);
447  }
449  impl_->peer_connection_manager_->GetIceCandidateList(peerid));
450  } else if (entry_point == "/api/hangup") {
451  std::string peerid;
452  if (!query_string_trimmed.empty()) {
453  CivetServer::getParam(query_string_trimmed.c_str(), "peerid",
454  peerid);
455  }
457  impl_->peer_connection_manager_->HangUp(peerid));
458  } else if (entry_point == "/api/call") {
459  std::string peerid;
460  std::string url;
461  std::string options;
462  if (!query_string_trimmed.empty()) {
463  CivetServer::getParam(query_string_trimmed.c_str(), "peerid",
464  peerid);
465  CivetServer::getParam(query_string_trimmed.c_str(), "url", url);
466  CivetServer::getParam(query_string_trimmed.c_str(), "options",
467  options);
468  }
469  result = utility::JsonToString(impl_->peer_connection_manager_->Call(
470  peerid, url, options, utility::StringToJson(data)));
471  } else if (entry_point == "/api/addIceCandidate") {
472  std::string peerid;
473  if (!query_string_trimmed.empty()) {
474  CivetServer::getParam(query_string_trimmed.c_str(), "peerid",
475  peerid);
476  }
478  impl_->peer_connection_manager_->AddIceCandidate(
479  peerid, utility::StringToJson(data)));
480  }
481 
482  utility::LogDebug("result: {}", result);
483 
484  return result;
485 }
486 
488  utility::LogInfo("WebRTC GUI backend enabled.");
490 }
491 
493  utility::LogInfo("WebRTCWindowSystem: HTTP handshake server disabled.");
494  impl_->http_handshake_enabled_ = false;
495 }
496 
497 void WebRTCWindowSystem::CloseWindowConnections(const std::string &window_uid) {
498  impl_->peer_connection_manager_->CloseWindowConnections(window_uid);
499 }
500 
501 } // namespace webrtc_server
502 } // namespace visualization
503 } // namespace cloudViewer
Window * o3d_window
std::function< void(std::shared_ptr< core::Tensor >)> callback
filament::Texture::InternalFormat format
int width
int height
int count
core::Tensor result
Definition: VtkUtils.cpp:76
void SetWindowSystem(std::shared_ptr< WindowSystem > ws)
void SetWindowSize(OSWindow w, int width, int height) override
void PostMouseEvent(OSWindow w, const MouseEvent &e)
WindowSystem::OSWindow GetOSWindow() const
Definition: Window.cpp:701
std::function< Json::Value(const struct mg_request_info *req_info, const Json::Value &)> HttpFunction
WebRTCWindowSystem is a BitmapWindowSystem with a WebRTC server that sends video frames to remote cli...
std::vector< std::string > GetWindowUIDs() const
List available windows.
std::string CallHttpAPI(const std::string &entry_point, const std::string &query_string="", const std::string &data="") const
Call PeerConnectionManager's web request API.
static std::shared_ptr< WebRTCWindowSystem > GetInstance()
void RegisterDataChannelMessageCallback(const std::string &class_name, const std::function< std::string(const std::string &)> callback)
void CloseWindowConnections(const std::string &window_uid)
Close all WebRTC connections that correspond to a Window.
void StartWebRTCServer()
Start WebRTC server in a background thread.
OSWindow CreateOSWindow(gui::Window *o3d_window, int width, int height, const char *title, int flags) override
void OnFrame(const std::string &window_uid, const std::shared_ptr< core::Tensor > &im)
Server -> client frame.
#define LogWarning(...)
Definition: Logging.h:72
#define LogInfo(...)
Definition: Logging.h:81
#define LogError(...)
Definition: Logging.h:60
#define LogDebug(...)
Definition: Logging.h:90
Helper functions for the ml ops.
void SplitString(std::vector< std::string > &tokens, const std::string &str, const std::string &delimiters=" ", bool trim_empty_str=true)
Definition: Helper.cpp:197
Json::Value StringToJson(const std::string &json_str)
Parse string and conver to Json::value. Throws exception if the conversion is invalid.
std::string JsonToString(const Json::Value &json)
Serialize a Json::Value to a string.
static const std::list< std::string > s_cloudViewer_ice_servers
static const std::list< std::string > s_public_ice_servers
Generic file read and write utility for python interface.
std::string to_string(const T &n)
Definition: Common.h:20
bool FromJson(const Json::Value &value)
Definition: Events.cpp:70
std::unordered_map< std::string, std::function< std::string(std::string)> > data_channel_message_callbacks_
std::unordered_map< WebRTCWindowSystem::OSWindow, std::string > os_window_to_uid_