mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 13:32:44 +00:00
Merge pull request #139 from OrcaSlicer/feat/webrtc-impl
feat: webrtc implementation
This commit is contained in:
@@ -801,6 +801,49 @@ find_package(OpenSSL REQUIRED)
|
|||||||
find_package(CURL REQUIRED)
|
find_package(CURL REQUIRED)
|
||||||
find_package(Freetype REQUIRED)
|
find_package(Freetype REQUIRED)
|
||||||
|
|
||||||
|
if (SLIC3R_GUI)
|
||||||
|
# LibDataChannel's installed export references its bundled dependencies,
|
||||||
|
# but does not install their CMake targets. Recreate those targets from
|
||||||
|
# the same dependency prefix before loading the LibDataChannel config.
|
||||||
|
if (NOT TARGET Usrsctp::usrsctp)
|
||||||
|
find_library(_ORCA_USRSCTP_LIBRARY NAMES usrsctp
|
||||||
|
PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH)
|
||||||
|
if (_ORCA_USRSCTP_LIBRARY)
|
||||||
|
add_library(Usrsctp::usrsctp UNKNOWN IMPORTED GLOBAL)
|
||||||
|
set_target_properties(Usrsctp::usrsctp PROPERTIES
|
||||||
|
IMPORTED_LOCATION "${_ORCA_USRSCTP_LIBRARY}"
|
||||||
|
IMPORTED_LINK_INTERFACE_LANGUAGES C
|
||||||
|
INTERFACE_LINK_LIBRARIES "Threads::Threads")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT TARGET libSRTP::srtp2)
|
||||||
|
find_library(_ORCA_SRTP_LIBRARY NAMES srtp2
|
||||||
|
PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH)
|
||||||
|
if (_ORCA_SRTP_LIBRARY)
|
||||||
|
add_library(libSRTP::srtp2 UNKNOWN IMPORTED GLOBAL)
|
||||||
|
set_target_properties(libSRTP::srtp2 PROPERTIES
|
||||||
|
IMPORTED_LOCATION "${_ORCA_SRTP_LIBRARY}"
|
||||||
|
IMPORTED_LINK_INTERFACE_LANGUAGES C
|
||||||
|
INTERFACE_LINK_LIBRARIES "OpenSSL::Crypto")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT TARGET LibJuice::LibJuice)
|
||||||
|
find_library(_ORCA_LIBJUICE_LIBRARY NAMES juice
|
||||||
|
PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH)
|
||||||
|
if (_ORCA_LIBJUICE_LIBRARY)
|
||||||
|
add_library(LibJuice::LibJuice UNKNOWN IMPORTED GLOBAL)
|
||||||
|
set_target_properties(LibJuice::LibJuice PROPERTIES
|
||||||
|
IMPORTED_LOCATION "${_ORCA_LIBJUICE_LIBRARY}"
|
||||||
|
IMPORTED_LINK_INTERFACE_LANGUAGES C
|
||||||
|
INTERFACE_LINK_LIBRARIES "Threads::Threads")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_package(LibDataChannel CONFIG REQUIRED)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
add_library(libcurl INTERFACE)
|
add_library(libcurl INTERFACE)
|
||||||
target_link_libraries(libcurl INTERFACE CURL::libcurl)
|
target_link_libraries(libcurl INTERFACE CURL::libcurl)
|
||||||
|
|||||||
Vendored
+3
@@ -389,6 +389,8 @@ if(NOT OPENSSL_FOUND)
|
|||||||
set(OPENSSL_PKG dep_OpenSSL)
|
set(OPENSSL_PKG dep_OpenSSL)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
include(DataChannel/DataChannel.cmake)
|
||||||
|
|
||||||
# we don't want to load a "wrong" openssl when loading curl
|
# we don't want to load a "wrong" openssl when loading curl
|
||||||
# so, just don't even bother
|
# so, just don't even bother
|
||||||
# ...i think this is how it works? change if wrong
|
# ...i think this is how it works? change if wrong
|
||||||
@@ -461,6 +463,7 @@ set(_dep_list
|
|||||||
dep_wxInspector
|
dep_wxInspector
|
||||||
dep_FFMPEG
|
dep_FFMPEG
|
||||||
dep_Assimp
|
dep_Assimp
|
||||||
|
dep_DataChannel
|
||||||
)
|
)
|
||||||
|
|
||||||
if (MSVC)
|
if (MSVC)
|
||||||
|
|||||||
Vendored
+20
@@ -0,0 +1,20 @@
|
|||||||
|
# libdatachannel is the native ICE/DTLS/SCTP/SRTP implementation used by the
|
||||||
|
# GUI WebRTC camera controller. Keep the source revision fixed: the signaling
|
||||||
|
# protocol is evolving independently of this transport dependency.
|
||||||
|
orcaslicer_add_cmake_project(DataChannel
|
||||||
|
CMAKE_ARGS
|
||||||
|
-DNO_EXAMPLES=ON
|
||||||
|
-DNO_TESTS=ON
|
||||||
|
-DNO_WEBSOCKET=ON
|
||||||
|
-DNO_MEDIA=OFF
|
||||||
|
-DUSE_NICE=OFF
|
||||||
|
-DUSE_SYSTEM_SRTP=OFF
|
||||||
|
-DUSE_SYSTEM_JUICE=OFF
|
||||||
|
-DUSE_SYSTEM_USRSCTP=OFF
|
||||||
|
-DOPENSSL_ROOT_DIR:PATH=${DESTDIR}
|
||||||
|
-DOPENSSL_USE_STATIC_LIBS=ON
|
||||||
|
GIT_REPOSITORY https://github.com/paullouisageneau/libdatachannel.git
|
||||||
|
GIT_TAG v0.22.2
|
||||||
|
GIT_SHALLOW ON
|
||||||
|
GIT_SUBMODULES_RECURSE ON
|
||||||
|
)
|
||||||
@@ -344,6 +344,8 @@ set(SLIC3R_GUI_SOURCES
|
|||||||
GUI/MediaFilePanel.h
|
GUI/MediaFilePanel.h
|
||||||
GUI/MediaPlayCtrl.cpp
|
GUI/MediaPlayCtrl.cpp
|
||||||
GUI/MediaPlayCtrl.h
|
GUI/MediaPlayCtrl.h
|
||||||
|
GUI/WebRtcMediaController.cpp
|
||||||
|
GUI/WebRtcMediaController.hpp
|
||||||
GUI/MeshUtils.cpp
|
GUI/MeshUtils.cpp
|
||||||
GUI/MeshUtils.hpp
|
GUI/MeshUtils.hpp
|
||||||
GUI/ModelMall.cpp
|
GUI/ModelMall.cpp
|
||||||
@@ -723,12 +725,15 @@ set(SLIC3R_GUI_SOURCES
|
|||||||
Utils/NetworkAgentFactory.cpp
|
Utils/NetworkAgentFactory.cpp
|
||||||
Utils/ICloudServiceAgent.hpp
|
Utils/ICloudServiceAgent.hpp
|
||||||
Utils/IPrinterAgent.hpp
|
Utils/IPrinterAgent.hpp
|
||||||
|
Utils/ICameraSignalingChannel.hpp
|
||||||
Utils/OrcaCloudServiceAgent.cpp
|
Utils/OrcaCloudServiceAgent.cpp
|
||||||
Utils/OrcaCloudServiceAgent.hpp
|
Utils/OrcaCloudServiceAgent.hpp
|
||||||
Utils/OrcaMqttConnection.cpp
|
Utils/OrcaMqttConnection.cpp
|
||||||
Utils/OrcaMqttConnection.hpp
|
Utils/OrcaMqttConnection.hpp
|
||||||
Utils/OrcaPrinterAgent.cpp
|
Utils/OrcaPrinterAgent.cpp
|
||||||
Utils/OrcaPrinterAgent.hpp
|
Utils/OrcaPrinterAgent.hpp
|
||||||
|
Utils/OrcaCloudSignalingChannel.cpp
|
||||||
|
Utils/OrcaCloudSignalingChannel.hpp
|
||||||
Utils/QidiPrinterAgent.cpp
|
Utils/QidiPrinterAgent.cpp
|
||||||
Utils/QidiPrinterAgent.hpp
|
Utils/QidiPrinterAgent.hpp
|
||||||
Utils/SnapmakerPrinterAgent.cpp
|
Utils/SnapmakerPrinterAgent.cpp
|
||||||
@@ -857,7 +862,7 @@ else()
|
|||||||
set(_opengl_link_lib OpenGL::GL)
|
set(_opengl_link_lib OpenGL::GL)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi mdns ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto noise::noise pybind11::embed)
|
target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi mdns ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto LibDataChannel::LibDataChannel noise::noise pybind11::embed)
|
||||||
|
|
||||||
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||||
# Linux finds wxWidgets in module mode, whose include dirs and definitions
|
# Linux finds wxWidgets in module mode, whose include dirs and definitions
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include <wx/mediactrl.h>
|
#include <wx/mediactrl.h>
|
||||||
#include <wx/uri.h>
|
#include <wx/uri.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <slic3r/Utils/IPrinterAgent.hpp>
|
#include <slic3r/Utils/IPrinterAgent.hpp>
|
||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
namespace Slic3r { namespace GUI {
|
||||||
@@ -10,6 +12,8 @@ namespace Slic3r { namespace GUI {
|
|||||||
class IMediaController
|
class IMediaController
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
virtual ~IMediaController() = default;
|
||||||
|
|
||||||
virtual void Load(wxURI url) = 0;
|
virtual void Load(wxURI url) = 0;
|
||||||
|
|
||||||
// The default keeps existing media controllers unaware of camera-specific modes.
|
// The default keeps existing media controllers unaware of camera-specific modes.
|
||||||
@@ -29,6 +33,13 @@ public:
|
|||||||
|
|
||||||
virtual wxSize GetVideoSize() const { return {}; };
|
virtual wxSize GetVideoSize() const { return {}; };
|
||||||
|
|
||||||
|
virtual void StartSession(std::unique_ptr<ICameraSignalingChannel> channel)
|
||||||
|
{
|
||||||
|
(void) channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void StopSession() {}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,11 @@ MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const w
|
|||||||
|
|
||||||
MediaPlayCtrl::~MediaPlayCtrl()
|
MediaPlayCtrl::~MediaPlayCtrl()
|
||||||
{
|
{
|
||||||
|
m_webrtc_stopping = true;
|
||||||
|
if (m_webrtc_ctrl)
|
||||||
|
m_webrtc_ctrl->StopSession();
|
||||||
|
m_media_ctrl->EndExternalStream();
|
||||||
|
m_webrtc_stopping = false;
|
||||||
{
|
{
|
||||||
boost::unique_lock lock(m_mutex);
|
boost::unique_lock lock(m_mutex);
|
||||||
m_tasks.push_back("<exit>");
|
m_tasks.push_back("<exit>");
|
||||||
@@ -159,7 +164,14 @@ CameraStreamMode MediaPlayCtrl::current_mode() const
|
|||||||
|
|
||||||
void MediaPlayCtrl::SetMachineObject(MachineObject* obj)
|
void MediaPlayCtrl::SetMachineObject(MachineObject* obj)
|
||||||
{
|
{
|
||||||
switch (current_mode()) {
|
const CameraStreamMode mode = current_mode();
|
||||||
|
if (mode != m_last_mode) {
|
||||||
|
if (m_last_state != MEDIASTATE_IDLE)
|
||||||
|
Stop(" ");
|
||||||
|
m_last_mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
case CameraStreamMode::http:
|
case CameraStreamMode::http:
|
||||||
case CameraStreamMode::http_snapshot:
|
case CameraStreamMode::http_snapshot:
|
||||||
case CameraStreamMode::rtsp: {
|
case CameraStreamMode::rtsp: {
|
||||||
@@ -184,6 +196,28 @@ void MediaPlayCtrl::SetMachineObject(MachineObject* obj)
|
|||||||
Play();
|
Play();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case CameraStreamMode::webrtc: {
|
||||||
|
std::string machine = obj ? obj->get_dev_id() : "";
|
||||||
|
m_camera_exists = obj != nullptr;
|
||||||
|
Enable(obj != nullptr);
|
||||||
|
const bool changed = machine != m_machine;
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::SetMachineObject webrtc: changed=" << changed
|
||||||
|
<< " last_state=" << m_last_state << " web_user_stopped=" << m_web_user_stopped;
|
||||||
|
m_machine = machine;
|
||||||
|
m_url.clear();
|
||||||
|
m_agent_camera_url.clear();
|
||||||
|
if (!changed) {
|
||||||
|
if (m_last_state == MEDIASTATE_IDLE && IsEnabled() && !m_web_user_stopped)
|
||||||
|
Play();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_web_user_stopped = false;
|
||||||
|
if (m_last_state != MEDIASTATE_IDLE)
|
||||||
|
Stop(" ");
|
||||||
|
if (IsEnabled())
|
||||||
|
Play();
|
||||||
|
return;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -319,6 +353,48 @@ void MediaPlayCtrl::Play()
|
|||||||
m_button_play->SetIcon("media_stop");
|
m_button_play->SetIcon("media_stop");
|
||||||
load();
|
load();
|
||||||
return;
|
return;
|
||||||
|
case CameraStreamMode::webrtc: {
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Play webrtc: last_state=" << m_last_state
|
||||||
|
<< " next_retry_valid=" << m_next_retry.IsValid()
|
||||||
|
<< " next_retry_future=" << (m_next_retry.IsValid() && wxDateTime::Now() < m_next_retry)
|
||||||
|
<< " failed_retry=" << m_failed_retry << " shown=" << IsShownOnScreen();
|
||||||
|
if (m_webrtc_ctrl && m_webrtc_ctrl->is_active()) {
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Play webrtc: session already active, ignoring";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_next_retry.IsValid() && wxDateTime::Now() < m_next_retry)
|
||||||
|
return;
|
||||||
|
if (!IsShownOnScreen() || m_last_state != MEDIASTATE_IDLE)
|
||||||
|
return;
|
||||||
|
m_failed_code = 0;
|
||||||
|
if (m_machine.empty() || !IsEnabled() || !m_camera_exists) {
|
||||||
|
Stop(_L("Please confirm if the printer is connected."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto agent = wxGetApp().getAgent();
|
||||||
|
auto channel = agent ? agent->create_camera_signaling_channel(m_machine) : nullptr;
|
||||||
|
if (!channel) {
|
||||||
|
Stop(_L("Sign in to OrcaCloud to view the camera."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!m_webrtc_ctrl) {
|
||||||
|
m_webrtc_ctrl = std::make_unique<WebRtcMediaController>(
|
||||||
|
[this](const wxImage& image, wxSize size) { m_media_ctrl->SetExternalFrame(image, size); },
|
||||||
|
[this, token = std::weak_ptr<int>(m_token)](WebRtcMediaController::Status status) {
|
||||||
|
if (token.expired())
|
||||||
|
return;
|
||||||
|
CallAfter([this, status] { on_webrtc_status(status); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
m_button_play->SetIcon("media_stop");
|
||||||
|
m_media_ctrl->BeginExternalStream();
|
||||||
|
m_last_state = MEDIASTATE_INITIALIZING;
|
||||||
|
SetStatus(_L("Initializing..."), false);
|
||||||
|
m_webrtc_stopping = false;
|
||||||
|
m_webrtc_ctrl->StartSession(std::move(channel));
|
||||||
|
m_webrtc_epoch = m_webrtc_ctrl->epoch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -463,6 +539,17 @@ void MediaPlayCtrl::StopWebStream()
|
|||||||
|
|
||||||
void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2)
|
void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2)
|
||||||
{
|
{
|
||||||
|
const bool webrtc_active = m_webrtc_ctrl && (m_last_mode == CameraStreamMode::webrtc ||
|
||||||
|
current_mode() == CameraStreamMode::webrtc);
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Stop: last_state=" << m_last_state
|
||||||
|
<< " webrtc_active=" << webrtc_active << " failed_code=" << m_failed_code
|
||||||
|
<< " msg='" << msg.ToUTF8().data() << "'";
|
||||||
|
if (webrtc_active) {
|
||||||
|
m_webrtc_stopping = true;
|
||||||
|
m_webrtc_ctrl->StopSession();
|
||||||
|
m_media_ctrl->EndExternalStream();
|
||||||
|
m_webrtc_stopping = false;
|
||||||
|
}
|
||||||
switch (current_mode()) {
|
switch (current_mode()) {
|
||||||
case CameraStreamMode::http:
|
case CameraStreamMode::http:
|
||||||
case CameraStreamMode::http_snapshot:
|
case CameraStreamMode::http_snapshot:
|
||||||
@@ -553,6 +640,27 @@ void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2)
|
|||||||
m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(5 * m_failed_retry);
|
m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(5 * m_failed_retry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MediaPlayCtrl::on_webrtc_status(WebRtcMediaController::Status status)
|
||||||
|
{
|
||||||
|
// Drop CallAfter-queued events from a superseded StartSession attempt.
|
||||||
|
if (status.epoch != m_webrtc_epoch)
|
||||||
|
return;
|
||||||
|
if (status.kind == WebRtcMediaController::Status::Connecting) {
|
||||||
|
m_last_state = MEDIASTATE_INITIALIZING;
|
||||||
|
SetStatus(_L("Initializing..."), false);
|
||||||
|
} else if (status.kind == WebRtcMediaController::Status::Playing) {
|
||||||
|
m_last_state = wxMEDIASTATE_PLAYING;
|
||||||
|
m_failed_code = 0;
|
||||||
|
m_failed_retry = 0;
|
||||||
|
SetStatus(_L("Playing..."), false);
|
||||||
|
} else if (status.kind == WebRtcMediaController::Status::Failed) {
|
||||||
|
m_failed_code = static_cast<int>(status.code) + 1;
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
// Status::Stopped needs no action: a genuine failure arrives as Failed, and
|
||||||
|
// a stop we initiated is already handled by Stop() itself.
|
||||||
|
}
|
||||||
|
|
||||||
void MediaPlayCtrl::TogglePlay()
|
void MediaPlayCtrl::TogglePlay()
|
||||||
{
|
{
|
||||||
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::TogglePlay";
|
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::TogglePlay";
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include "wxMediaCtrl3.h"
|
#include "wxMediaCtrl3.h"
|
||||||
#include "IMediaController.hpp"
|
#include "IMediaController.hpp"
|
||||||
|
#include "WebRtcMediaController.hpp"
|
||||||
#include "slic3r/Utils/IPrinterAgent.hpp"
|
#include "slic3r/Utils/IPrinterAgent.hpp"
|
||||||
|
|
||||||
#include <wx/panel.h>
|
#include <wx/panel.h>
|
||||||
@@ -60,6 +61,7 @@ protected:
|
|||||||
void TogglePlay();
|
void TogglePlay();
|
||||||
|
|
||||||
void SetStatus(wxString const &msg, bool hyperlink = true);
|
void SetStatus(wxString const &msg, bool hyperlink = true);
|
||||||
|
void on_webrtc_status(WebRtcMediaController::Status status);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void load();
|
void load();
|
||||||
@@ -85,6 +87,10 @@ private:
|
|||||||
|
|
||||||
wxMediaCtrl3 * m_media_ctrl;
|
wxMediaCtrl3 * m_media_ctrl;
|
||||||
IMediaController * m_web_ctrl = nullptr;
|
IMediaController * m_web_ctrl = nullptr;
|
||||||
|
std::unique_ptr<WebRtcMediaController> m_webrtc_ctrl;
|
||||||
|
CameraStreamMode m_last_mode = CameraStreamMode::none;
|
||||||
|
bool m_webrtc_stopping = false;
|
||||||
|
std::uint64_t m_webrtc_epoch = 0;
|
||||||
std::string m_agent_camera_url;
|
std::string m_agent_camera_url;
|
||||||
bool m_web_user_stopped = false;
|
bool m_web_user_stopped = false;
|
||||||
wxMediaState m_last_state = MEDIASTATE_IDLE;
|
wxMediaState m_last_state = MEDIASTATE_IDLE;
|
||||||
|
|||||||
@@ -0,0 +1,398 @@
|
|||||||
|
#include "WebRtcMediaController.hpp"
|
||||||
|
|
||||||
|
#include <rtc/common.hpp>
|
||||||
|
#include <rtc/rtc.hpp>
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <wx/mstream.h>
|
||||||
|
|
||||||
|
#include <boost/log/trivial.hpp>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void init_rtc_logger_once()
|
||||||
|
{
|
||||||
|
static std::once_flag flag;
|
||||||
|
std::call_once(flag, [] {
|
||||||
|
rtc::InitLogger(rtc::LogLevel::Verbose, [](rtc::LogLevel level, std::string message) {
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "[rtc:" << static_cast<int>(level) << "] " << message;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
WebRtcMediaController::WebRtcMediaController(std::function<void(const wxImage&, wxSize)> frame_sink,
|
||||||
|
std::function<void(Status)> on_status)
|
||||||
|
: m_frame_sink(std::move(frame_sink))
|
||||||
|
, m_on_status(std::move(on_status))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
WebRtcMediaController::~WebRtcMediaController()
|
||||||
|
{
|
||||||
|
StopSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebRtcMediaController::report(Status status)
|
||||||
|
{
|
||||||
|
status.epoch = m_epoch.load();
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "WebRTC: report kind=" << static_cast<int>(status.kind)
|
||||||
|
<< " code=" << static_cast<int>(status.code) << " epoch=" << status.epoch;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
if (status.kind == Status::Connecting)
|
||||||
|
m_state = static_cast<wxMediaState>(4);
|
||||||
|
else if (status.kind == Status::Playing)
|
||||||
|
m_state = wxMEDIASTATE_PLAYING;
|
||||||
|
else
|
||||||
|
m_state = static_cast<wxMediaState>(3);
|
||||||
|
}
|
||||||
|
if (m_on_status)
|
||||||
|
m_on_status(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebRtcMediaController::StartSession(std::unique_ptr<ICameraSignalingChannel> channel)
|
||||||
|
{
|
||||||
|
// Tear down any previous attempt WITHOUT notifying: the Stopped that would
|
||||||
|
// otherwise be delivered (async, via CallAfter) races the new attempt's
|
||||||
|
// Connecting and makes the consumer cancel a session that is mid-connect.
|
||||||
|
teardown(false);
|
||||||
|
if (!channel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_epoch.fetch_add(1);
|
||||||
|
m_alive.store(true);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_signaling = std::move(channel);
|
||||||
|
m_jpeg_queue.clear();
|
||||||
|
m_pending_candidates.clear();
|
||||||
|
m_remote_description_set = false;
|
||||||
|
m_video_size = wxDefaultSize;
|
||||||
|
m_has_frame = false;
|
||||||
|
m_last_frame_time = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
ICameraSignalingChannel* signaling = nullptr;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
signaling = m_signaling.get();
|
||||||
|
}
|
||||||
|
signaling->on_ready = [this](std::vector<CameraIceServer> servers) {
|
||||||
|
if (m_alive.load())
|
||||||
|
on_ready(std::move(servers));
|
||||||
|
};
|
||||||
|
signaling->on_answer = [this](std::string sdp) {
|
||||||
|
if (m_alive.load())
|
||||||
|
on_answer(std::move(sdp));
|
||||||
|
};
|
||||||
|
signaling->on_ice = [this](std::string candidate, std::string mid) {
|
||||||
|
if (m_alive.load())
|
||||||
|
on_ice(std::move(candidate), std::move(mid));
|
||||||
|
};
|
||||||
|
signaling->on_unavailable = [this](CameraUnavailableReason reason, std::string detail) {
|
||||||
|
if (m_alive.load())
|
||||||
|
on_unavailable(reason, std::move(detail));
|
||||||
|
};
|
||||||
|
|
||||||
|
m_decode_thread = std::thread([this] { decode_loop(); });
|
||||||
|
report({Status::Connecting});
|
||||||
|
signaling->open();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebRtcMediaController::StopSession()
|
||||||
|
{
|
||||||
|
teardown(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebRtcMediaController::teardown(bool notify)
|
||||||
|
{
|
||||||
|
const bool was_alive = m_alive.exchange(false);
|
||||||
|
if (!was_alive && !m_decode_thread.joinable())
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_cond.notify_all();
|
||||||
|
std::unique_ptr<ICameraSignalingChannel> signaling;
|
||||||
|
std::shared_ptr<rtc::PeerConnection> peer_connection;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
signaling = std::move(m_signaling);
|
||||||
|
peer_connection = std::move(m_peer_connection);
|
||||||
|
m_data_channel.reset();
|
||||||
|
}
|
||||||
|
if (signaling)
|
||||||
|
signaling->close();
|
||||||
|
if (peer_connection)
|
||||||
|
peer_connection->close();
|
||||||
|
if (m_decode_thread.joinable())
|
||||||
|
m_decode_thread.join();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_jpeg_queue.clear();
|
||||||
|
}
|
||||||
|
if (was_alive && notify)
|
||||||
|
report({Status::Stopped});
|
||||||
|
}
|
||||||
|
|
||||||
|
wxMediaState WebRtcMediaController::GetState()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
return m_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
wxSize WebRtcMediaController::GetVideoSize() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
return m_video_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebRtcMediaController::bind_data_channel(const std::shared_ptr<rtc::DataChannel>& dc)
|
||||||
|
{
|
||||||
|
const std::string label = dc->label();
|
||||||
|
dc->onOpen([this, label] { BOOST_LOG_TRIVIAL(info) << "WebRTC: data channel '" << label << "' open"; });
|
||||||
|
dc->onClosed([this, label] { BOOST_LOG_TRIVIAL(info) << "WebRTC: data channel '" << label << "' closed"; });
|
||||||
|
dc->onError([label](std::string e) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "WebRTC: data channel '" << label << "' error: " << e;
|
||||||
|
});
|
||||||
|
dc->onMessage(
|
||||||
|
[this](rtc::binary data) {
|
||||||
|
if (m_alive.load())
|
||||||
|
enqueue_jpeg(std::vector<std::byte>(data.begin(), data.end()));
|
||||||
|
},
|
||||||
|
[](rtc::string) {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebRtcMediaController::on_ready(std::vector<CameraIceServer> servers)
|
||||||
|
{
|
||||||
|
init_rtc_logger_once();
|
||||||
|
rtc::Configuration configuration;
|
||||||
|
// Allow complete-JPEG DataChannel messages up to 1 MiB. This value is
|
||||||
|
// advertised in SDP and becomes the upper bound for frames OrcaSonar can
|
||||||
|
// send to OrcaSlicer.
|
||||||
|
configuration.maxMessageSize = 1024 * 1024;
|
||||||
|
for (const CameraIceServer& server : servers) {
|
||||||
|
try {
|
||||||
|
rtc::IceServer ice_server(server.urls);
|
||||||
|
ice_server.username = server.username;
|
||||||
|
ice_server.password = server.credential;
|
||||||
|
configuration.iceServers.emplace_back(std::move(ice_server));
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "WebRTC: invalid ICE server: " << e.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
configuration.iceServers.emplace_back("stun:stun.cloudflare.com:3478");
|
||||||
|
configuration.iceServers.emplace_back("stun:stun.l.google.com:19302");
|
||||||
|
|
||||||
|
auto peer_connection = std::make_shared<rtc::PeerConnection>(std::move(configuration));
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "WebRTC: creating peer connection with " << configuration.iceServers.size()
|
||||||
|
<< " ice servers";
|
||||||
|
peer_connection->onLocalDescription([this](rtc::Description description) {
|
||||||
|
if (!m_alive.load())
|
||||||
|
return;
|
||||||
|
const std::string sdp(description);
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "WebRTC: local description ready (" << description.typeString()
|
||||||
|
<< "), OFFER SDP:\n" << sdp;
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
if (m_signaling)
|
||||||
|
m_signaling->send_offer(sdp);
|
||||||
|
});
|
||||||
|
peer_connection->onLocalCandidate([this](rtc::Candidate candidate) {
|
||||||
|
if (!m_alive.load())
|
||||||
|
return;
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
if (m_signaling)
|
||||||
|
m_signaling->send_ice(std::string(candidate), candidate.mid());
|
||||||
|
});
|
||||||
|
peer_connection->onStateChange([this](rtc::PeerConnection::State state) {
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "WebRTC: peer state -> " << static_cast<int>(state);
|
||||||
|
if (!m_alive.load())
|
||||||
|
return;
|
||||||
|
if (state == rtc::PeerConnection::State::Failed || state == rtc::PeerConnection::State::Disconnected)
|
||||||
|
report({Status::Failed, Status::ICE_FAILED});
|
||||||
|
});
|
||||||
|
peer_connection->onGatheringStateChange([](rtc::PeerConnection::GatheringState state) {
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "WebRTC: gathering state -> " << static_cast<int>(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Accept a DataChannel opened by the remote peer (OrcaSonar may create the
|
||||||
|
// "camera" channel from its side rather than answering the one we offer).
|
||||||
|
peer_connection->onDataChannel([this](std::shared_ptr<rtc::DataChannel> dc) {
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "WebRTC: remote opened data channel '" << dc->label() << "'";
|
||||||
|
bind_data_channel(dc);
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_data_channel = std::move(dc);
|
||||||
|
});
|
||||||
|
|
||||||
|
rtc::DataChannelInit init;
|
||||||
|
init.reliability.unordered = true;
|
||||||
|
// init.reliability.maxPacketLifeTime = std::chrono::milliseconds(350);
|
||||||
|
// Request one complete JPEG frame per DataChannel message. OrcaSonar
|
||||||
|
// keeps the legacy chunked protocol for clients that omit this property.
|
||||||
|
init.protocol = "orca-jpeg";
|
||||||
|
auto data_channel = peer_connection->createDataChannel("camera", init);
|
||||||
|
if (data_channel)
|
||||||
|
bind_data_channel(data_channel);
|
||||||
|
|
||||||
|
// Camera media is carried as one complete JPEG per DataChannel message;
|
||||||
|
// no RTP video track or application-level framing is required.
|
||||||
|
|
||||||
|
std::shared_ptr<rtc::PeerConnection> peer_for_description;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
if (!m_alive.load())
|
||||||
|
return;
|
||||||
|
m_peer_connection = std::move(peer_connection);
|
||||||
|
peer_for_description = m_peer_connection;
|
||||||
|
m_data_channel = std::move(data_channel);
|
||||||
|
}
|
||||||
|
if (peer_for_description)
|
||||||
|
peer_for_description->setLocalDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebRtcMediaController::on_answer(std::string sdp)
|
||||||
|
{
|
||||||
|
std::shared_ptr<rtc::PeerConnection> peer_connection;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
peer_connection = m_peer_connection;
|
||||||
|
}
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "WebRTC: applying remote answer (" << sdp.size() << " bytes), ANSWER SDP:\n" << sdp;
|
||||||
|
if (!peer_connection)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
peer_connection->setRemoteDescription(rtc::Description(sdp, "answer"));
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "WebRTC: setRemoteDescription failed: " << e.what();
|
||||||
|
report({Status::Failed, Status::ICE_FAILED});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush any remote candidates that arrived before the answer.
|
||||||
|
std::vector<std::pair<std::string, std::string>> pending;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_remote_description_set = true;
|
||||||
|
pending.swap(m_pending_candidates);
|
||||||
|
}
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "WebRTC: remote description set, flushing " << pending.size()
|
||||||
|
<< " buffered candidate(s)";
|
||||||
|
for (const auto& c : pending) {
|
||||||
|
try {
|
||||||
|
peer_connection->addRemoteCandidate(rtc::Candidate(c.first, c.second));
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "WebRTC: addRemoteCandidate (buffered) failed: " << e.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebRtcMediaController::on_ice(std::string candidate, std::string mid)
|
||||||
|
{
|
||||||
|
std::shared_ptr<rtc::PeerConnection> peer_connection;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
if (!m_remote_description_set) {
|
||||||
|
m_pending_candidates.emplace_back(std::move(candidate), std::move(mid));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
peer_connection = m_peer_connection;
|
||||||
|
}
|
||||||
|
if (!peer_connection)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
peer_connection->addRemoteCandidate(rtc::Candidate(candidate, mid));
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "WebRTC: addRemoteCandidate failed: " << e.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebRtcMediaController::on_unavailable(CameraUnavailableReason reason, std::string detail)
|
||||||
|
{
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "WebRTC camera unavailable: " << detail;
|
||||||
|
Status::Code code = Status::UNAVAILABLE_ERROR;
|
||||||
|
if (reason == CameraUnavailableReason::Busy)
|
||||||
|
code = Status::UNAVAILABLE_BUSY;
|
||||||
|
else if (reason == CameraUnavailableReason::Disabled)
|
||||||
|
code = Status::UNAVAILABLE_DISABLED;
|
||||||
|
else if (reason == CameraUnavailableReason::Closed)
|
||||||
|
code = Status::SIGNALING_CLOSED;
|
||||||
|
report({Status::Failed, code});
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebRtcMediaController::enqueue_jpeg(std::vector<std::byte> jpeg)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
if (m_jpeg_queue.size() >= 4)
|
||||||
|
m_jpeg_queue.pop_front();
|
||||||
|
m_jpeg_queue.emplace_back(std::move(jpeg));
|
||||||
|
m_cond.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebRtcMediaController::deliver_jpeg(std::vector<std::byte> jpeg)
|
||||||
|
{
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
if (m_last_frame_time != std::chrono::steady_clock::time_point{} &&
|
||||||
|
now - m_last_frame_time < std::chrono::milliseconds(33))
|
||||||
|
return;
|
||||||
|
m_last_frame_time = now;
|
||||||
|
}
|
||||||
|
wxMemoryInputStream stream(jpeg.data(), jpeg.size());
|
||||||
|
wxImage image;
|
||||||
|
if (!image.LoadFile(stream, wxBITMAP_TYPE_JPEG)) {
|
||||||
|
report({Status::Failed, Status::DECODE_ERROR});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool first_frame = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_video_size = image.GetSize();
|
||||||
|
first_frame = !m_has_frame;
|
||||||
|
m_has_frame = true;
|
||||||
|
}
|
||||||
|
if (m_frame_sink)
|
||||||
|
m_frame_sink(image, image.GetSize());
|
||||||
|
if (first_frame)
|
||||||
|
report({Status::Playing});
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebRtcMediaController::decode_loop()
|
||||||
|
{
|
||||||
|
int stall_polls = 0;
|
||||||
|
std::unique_lock<std::mutex> lock(m_mutex);
|
||||||
|
while (m_alive.load()) {
|
||||||
|
const bool woke = m_cond.wait_for(lock, std::chrono::seconds(2), [this] {
|
||||||
|
return !m_alive.load() || !m_jpeg_queue.empty();
|
||||||
|
});
|
||||||
|
if (!m_alive.load())
|
||||||
|
break;
|
||||||
|
if (!woke && !m_has_frame) {
|
||||||
|
const int pc_state = m_peer_connection ? static_cast<int>(m_peer_connection->state()) : -1;
|
||||||
|
std::string dc = "none";
|
||||||
|
if (m_data_channel)
|
||||||
|
dc = "label='" + m_data_channel->label() + "' open=" +
|
||||||
|
(m_data_channel->isOpen() ? "1" : "0");
|
||||||
|
lock.unlock();
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "WebRTC: waiting for frames; peer_state=" << pc_state
|
||||||
|
<< " data_channel=" << dc;
|
||||||
|
if (++stall_polls >= 8) { // ~16s connected with no frame -> give up so the UI can retry
|
||||||
|
report({Status::Failed, Status::TIMEOUT});
|
||||||
|
lock.lock();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lock.lock();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
stall_polls = 0;
|
||||||
|
if (!m_jpeg_queue.empty()) {
|
||||||
|
auto jpeg = std::move(m_jpeg_queue.front());
|
||||||
|
m_jpeg_queue.pop_front();
|
||||||
|
lock.unlock();
|
||||||
|
deliver_jpeg(std::move(jpeg));
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}} // namespace Slic3r::GUI
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "IMediaController.hpp"
|
||||||
|
|
||||||
|
#include <wx/image.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace rtc {
|
||||||
|
class DataChannel;
|
||||||
|
class PeerConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
class WebRtcMediaController : public IMediaController {
|
||||||
|
public:
|
||||||
|
struct Status {
|
||||||
|
enum Kind { Connecting, Playing, Stopped, Failed } kind = Stopped;
|
||||||
|
enum Code {
|
||||||
|
ICE_FAILED,
|
||||||
|
SIGNALING_CLOSED,
|
||||||
|
UNAVAILABLE_BUSY,
|
||||||
|
UNAVAILABLE_ERROR,
|
||||||
|
UNAVAILABLE_DISABLED,
|
||||||
|
DECODE_ERROR,
|
||||||
|
TIMEOUT,
|
||||||
|
} code = ICE_FAILED;
|
||||||
|
// Identifies the StartSession attempt this status belongs to, so the
|
||||||
|
// consumer can drop CallAfter-queued events from a superseded attempt.
|
||||||
|
std::uint64_t epoch = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
WebRtcMediaController(std::function<void(const wxImage&, wxSize)> frame_sink,
|
||||||
|
std::function<void(Status)> on_status);
|
||||||
|
~WebRtcMediaController() override;
|
||||||
|
|
||||||
|
void StartSession(std::unique_ptr<ICameraSignalingChannel> channel) override;
|
||||||
|
void StopSession() override;
|
||||||
|
std::uint64_t epoch() const { return m_epoch.load(); }
|
||||||
|
bool is_active() const { return m_alive.load(); }
|
||||||
|
|
||||||
|
void Load(wxURI) override {}
|
||||||
|
void Play() override {}
|
||||||
|
void Stop() override { StopSession(); }
|
||||||
|
wxMediaState GetState() override;
|
||||||
|
wxSize GetVideoSize() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void teardown(bool notify);
|
||||||
|
void report(Status status);
|
||||||
|
void bind_data_channel(const std::shared_ptr<rtc::DataChannel>& dc);
|
||||||
|
void on_ready(std::vector<CameraIceServer> servers);
|
||||||
|
void on_answer(std::string sdp);
|
||||||
|
void on_ice(std::string candidate, std::string mid);
|
||||||
|
void on_unavailable(CameraUnavailableReason reason, std::string detail);
|
||||||
|
void decode_loop();
|
||||||
|
void enqueue_jpeg(std::vector<std::byte> jpeg);
|
||||||
|
void deliver_jpeg(std::vector<std::byte> jpeg);
|
||||||
|
|
||||||
|
mutable std::mutex m_mutex;
|
||||||
|
std::condition_variable m_cond;
|
||||||
|
std::deque<std::vector<std::byte>> m_jpeg_queue;
|
||||||
|
// Remote candidates can arrive before the answer; libdatachannel rejects
|
||||||
|
// addRemoteCandidate until a remote description is set, so buffer them.
|
||||||
|
std::vector<std::pair<std::string, std::string>> m_pending_candidates;
|
||||||
|
bool m_remote_description_set = false;
|
||||||
|
std::unique_ptr<ICameraSignalingChannel> m_signaling;
|
||||||
|
std::shared_ptr<rtc::PeerConnection> m_peer_connection;
|
||||||
|
std::shared_ptr<rtc::DataChannel> m_data_channel;
|
||||||
|
std::thread m_decode_thread;
|
||||||
|
std::atomic<bool> m_alive{false};
|
||||||
|
std::atomic<std::uint64_t> m_epoch{0};
|
||||||
|
wxMediaState m_state = static_cast<wxMediaState>(3);
|
||||||
|
wxSize m_video_size = wxDefaultSize;
|
||||||
|
std::function<void(const wxImage&, wxSize)> m_frame_sink;
|
||||||
|
std::function<void(Status)> m_on_status;
|
||||||
|
bool m_has_frame = false;
|
||||||
|
std::chrono::steady_clock::time_point m_last_frame_time{};
|
||||||
|
};
|
||||||
|
|
||||||
|
}} // namespace Slic3r::GUI
|
||||||
@@ -46,9 +46,13 @@ wxMediaCtrl3::~wxMediaCtrl3()
|
|||||||
m_thread.join();
|
m_thread.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void adjust_frame_size(wxSize& frame, wxSize const& video, wxSize const& window);
|
||||||
|
|
||||||
void wxMediaCtrl3::Load(wxURI url)
|
void wxMediaCtrl3::Load(wxURI url)
|
||||||
{
|
{
|
||||||
std::unique_lock<std::mutex> lk(m_mutex);
|
std::unique_lock<std::mutex> lk(m_mutex);
|
||||||
|
if (m_external)
|
||||||
|
return;
|
||||||
m_video_size = wxDefaultSize;
|
m_video_size = wxDefaultSize;
|
||||||
m_error = 0;
|
m_error = 0;
|
||||||
m_url.reset(new wxURI(url));
|
m_url.reset(new wxURI(url));
|
||||||
@@ -58,6 +62,8 @@ void wxMediaCtrl3::Load(wxURI url)
|
|||||||
void wxMediaCtrl3::Play()
|
void wxMediaCtrl3::Play()
|
||||||
{
|
{
|
||||||
std::unique_lock<std::mutex> lk(m_mutex);
|
std::unique_lock<std::mutex> lk(m_mutex);
|
||||||
|
if (m_external)
|
||||||
|
return;
|
||||||
if (m_state != wxMEDIASTATE_PLAYING) {
|
if (m_state != wxMEDIASTATE_PLAYING) {
|
||||||
m_state = wxMEDIASTATE_PLAYING;
|
m_state = wxMEDIASTATE_PLAYING;
|
||||||
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
|
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
|
||||||
@@ -77,6 +83,62 @@ void wxMediaCtrl3::Stop()
|
|||||||
Refresh();
|
Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void wxMediaCtrl3::SetExternalFrame(const wxImage& frame, wxSize videoSize)
|
||||||
|
{
|
||||||
|
if (!frame.IsOk())
|
||||||
|
return;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lk(m_mutex);
|
||||||
|
if (!m_external)
|
||||||
|
return;
|
||||||
|
m_frame = frame;
|
||||||
|
m_video_size = videoSize.IsFullySpecified() ? videoSize : frame.GetSize();
|
||||||
|
adjust_frame_size(m_frame_size, m_video_size, GetSize());
|
||||||
|
}
|
||||||
|
CallAfter([this] { Refresh(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
void wxMediaCtrl3::SetExternalFrame(const wxBitmap& frame, wxSize videoSize)
|
||||||
|
{
|
||||||
|
if (!frame.IsOk())
|
||||||
|
return;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lk(m_mutex);
|
||||||
|
if (!m_external)
|
||||||
|
return;
|
||||||
|
m_frame = frame;
|
||||||
|
m_video_size = videoSize.IsFullySpecified() ? videoSize : frame.GetSize();
|
||||||
|
adjust_frame_size(m_frame_size, m_video_size, GetSize());
|
||||||
|
}
|
||||||
|
CallAfter([this] { Refresh(); });
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void wxMediaCtrl3::BeginExternalStream()
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lk(m_mutex);
|
||||||
|
m_external = true;
|
||||||
|
m_url.reset();
|
||||||
|
m_active_url.reset();
|
||||||
|
m_video_size = wxDefaultSize;
|
||||||
|
m_frame = wxImage(m_idle_image);
|
||||||
|
m_cond.notify_all();
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void wxMediaCtrl3::EndExternalStream()
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lk(m_mutex);
|
||||||
|
m_external = false;
|
||||||
|
m_url.reset();
|
||||||
|
m_active_url.reset();
|
||||||
|
m_video_size = wxDefaultSize;
|
||||||
|
m_frame = wxImage(m_idle_image);
|
||||||
|
m_cond.notify_all();
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
void wxMediaCtrl3::SetIdleImage(wxString const &image)
|
void wxMediaCtrl3::SetIdleImage(wxString const &image)
|
||||||
{
|
{
|
||||||
if (m_idle_image == image)
|
if (m_idle_image == image)
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, in
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#ifndef _WIN32
|
|
||||||
#include <wx/image.h>
|
#include <wx/image.h>
|
||||||
#endif
|
|
||||||
#include "Printer/BambuTunnel.h"
|
#include "Printer/BambuTunnel.h"
|
||||||
|
|
||||||
class AVVideoDecoder;
|
class AVVideoDecoder;
|
||||||
@@ -39,6 +37,16 @@ public:
|
|||||||
|
|
||||||
void Stop();
|
void Stop();
|
||||||
|
|
||||||
|
// Render frames supplied by a controller which owns its own transport.
|
||||||
|
// The frame is copied while m_mutex is held; callers may release it after
|
||||||
|
// this method returns.
|
||||||
|
void SetExternalFrame(const wxImage& frame, wxSize videoSize);
|
||||||
|
#ifdef _WIN32
|
||||||
|
void SetExternalFrame(const wxBitmap& frame, wxSize videoSize);
|
||||||
|
#endif
|
||||||
|
void BeginExternalStream();
|
||||||
|
void EndExternalStream();
|
||||||
|
|
||||||
void SetIdleImage(wxString const & image);
|
void SetIdleImage(wxString const & image);
|
||||||
|
|
||||||
wxMediaState GetState();
|
wxMediaState GetState();
|
||||||
@@ -78,6 +86,7 @@ private:
|
|||||||
|
|
||||||
std::shared_ptr<wxURI> m_url;
|
std::shared_ptr<wxURI> m_url;
|
||||||
std::shared_ptr<wxURI> m_active_url;
|
std::shared_ptr<wxURI> m_active_url;
|
||||||
|
bool m_external = false;
|
||||||
std::uint64_t m_last_PTS{0};
|
std::uint64_t m_last_PTS{0};
|
||||||
std::chrono::system_clock::time_point m_last_PTS_expected;
|
std::chrono::system_clock::time_point m_last_PTS_expected;
|
||||||
std::chrono::system_clock::time_point m_last_PTS_practical;
|
std::chrono::system_clock::time_point m_last_PTS_practical;
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
struct CameraIceServer {
|
||||||
|
std::string urls;
|
||||||
|
std::string username;
|
||||||
|
std::string credential;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CameraUnavailableReason {
|
||||||
|
Busy,
|
||||||
|
Error,
|
||||||
|
Disabled,
|
||||||
|
Closed,
|
||||||
|
};
|
||||||
|
|
||||||
|
class ICameraSignalingChannel {
|
||||||
|
public:
|
||||||
|
virtual ~ICameraSignalingChannel() = default;
|
||||||
|
|
||||||
|
virtual void open() = 0;
|
||||||
|
virtual void close() = 0;
|
||||||
|
virtual void send_offer(std::string sdp) = 0;
|
||||||
|
virtual void send_ice(std::string candidate, std::string mid) = 0;
|
||||||
|
|
||||||
|
// These callbacks are invoked by the channel's worker thread. Consumers
|
||||||
|
// must marshal UI work to the GUI thread themselves.
|
||||||
|
std::function<void(std::vector<CameraIceServer>)> on_ready;
|
||||||
|
std::function<void(std::string)> on_answer;
|
||||||
|
std::function<void(std::string, std::string)> on_ice;
|
||||||
|
std::function<void(CameraUnavailableReason, std::string)> on_unavailable;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include "ICameraSignalingChannel.hpp"
|
||||||
|
|
||||||
#if 1
|
#if 1
|
||||||
|
|
||||||
@@ -382,6 +383,15 @@ public:
|
|||||||
* Only meaningful when get_camera_stream_mode() returns an HTTP or RTSP mode.
|
* Only meaningful when get_camera_stream_mode() returns an HTTP or RTSP mode.
|
||||||
*/
|
*/
|
||||||
virtual std::string get_camera_url() const { return {}; }
|
virtual std::string get_camera_url() const { return {}; }
|
||||||
|
|
||||||
|
// Optional native camera signaling. Plugin agents retain the default
|
||||||
|
// nullptr until a plugin-facing WebRTC contract is defined.
|
||||||
|
virtual std::unique_ptr<ICameraSignalingChannel>
|
||||||
|
create_camera_signaling_channel(const std::string& dev_id)
|
||||||
|
{
|
||||||
|
(void) dev_id;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Slic3r
|
} // namespace Slic3r
|
||||||
|
|||||||
@@ -1040,6 +1040,14 @@ std::string NetworkAgent::get_local_camera_stream_url() const
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<ICameraSignalingChannel>
|
||||||
|
NetworkAgent::create_camera_signaling_channel(const std::string& dev_id)
|
||||||
|
{
|
||||||
|
if (m_printer_agent)
|
||||||
|
return m_printer_agent->create_camera_signaling_channel(dev_id);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
int NetworkAgent::request_bind_ticket(std::string* ticket)
|
int NetworkAgent::request_bind_ticket(std::string* ticket)
|
||||||
{
|
{
|
||||||
if (m_printer_agent)
|
if (m_printer_agent)
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ public:
|
|||||||
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull);
|
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull);
|
||||||
CameraStreamMode get_camera_stream_mode() const;
|
CameraStreamMode get_camera_stream_mode() const;
|
||||||
std::string get_local_camera_stream_url() const;
|
std::string get_local_camera_stream_url() const;
|
||||||
|
std::unique_ptr<ICameraSignalingChannel> create_camera_signaling_channel(const std::string& dev_id);
|
||||||
int request_bind_ticket(std::string* ticket);
|
int request_bind_ticket(std::string* ticket);
|
||||||
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback);
|
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
#include "OrcaCloudSignalingChannel.hpp"
|
||||||
|
|
||||||
|
#include "Http.hpp"
|
||||||
|
|
||||||
|
#include <boost/asio/connect.hpp>
|
||||||
|
#include <boost/asio/ip/tcp.hpp>
|
||||||
|
#include <boost/asio/post.hpp>
|
||||||
|
#include <boost/beast/core.hpp>
|
||||||
|
#include <boost/log/trivial.hpp>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <openssl/ssl.h>
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
OrcaCloudSignalingChannel::OrcaCloudSignalingChannel(std::shared_ptr<ICloudServiceAgent> cloud, std::string dev_id)
|
||||||
|
: m_cloud(std::move(cloud))
|
||||||
|
, m_dev_id(std::move(dev_id))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
OrcaCloudSignalingChannel::~OrcaCloudSignalingChannel()
|
||||||
|
{
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OrcaCloudSignalingChannel::open()
|
||||||
|
{
|
||||||
|
bool expected = false;
|
||||||
|
if (!m_open.compare_exchange_strong(expected, true))
|
||||||
|
return;
|
||||||
|
m_stop.store(false);
|
||||||
|
m_thread = std::thread([this] { run(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void OrcaCloudSignalingChannel::close()
|
||||||
|
{
|
||||||
|
m_stop.store(true);
|
||||||
|
std::shared_ptr<Connection> conn;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
conn = m_conn;
|
||||||
|
}
|
||||||
|
if (conn) {
|
||||||
|
// Established session: close the socket on the io_context's own thread so
|
||||||
|
// the pending async_read completes and io_context.run() unwinds.
|
||||||
|
boost::asio::post(conn->io_context, [conn] {
|
||||||
|
boost::system::error_code ec;
|
||||||
|
boost::beast::get_lowest_layer(conn->websocket).cancel(ec);
|
||||||
|
boost::beast::get_lowest_layer(conn->websocket).close(ec);
|
||||||
|
});
|
||||||
|
// Pre-run() phase (still in the synchronous connect/handshake): best-effort
|
||||||
|
// direct interruption.
|
||||||
|
boost::system::error_code ec;
|
||||||
|
boost::beast::get_lowest_layer(conn->websocket).cancel(ec);
|
||||||
|
boost::beast::get_lowest_layer(conn->websocket).close(ec);
|
||||||
|
}
|
||||||
|
if (m_thread.joinable())
|
||||||
|
m_thread.join();
|
||||||
|
m_open.store(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OrcaCloudSignalingChannel::send_offer(std::string sdp)
|
||||||
|
{
|
||||||
|
send_json(nlohmann::json{{"type", "webrtc.offer"}, {"sdp", std::move(sdp)}}.dump());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OrcaCloudSignalingChannel::send_ice(std::string candidate, std::string mid)
|
||||||
|
{
|
||||||
|
send_json(nlohmann::json{{"type", "webrtc.ice"},
|
||||||
|
{"candidate", std::move(candidate)},
|
||||||
|
{"sdpMid", std::move(mid)}}
|
||||||
|
.dump());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string OrcaCloudSignalingChannel::encode_path_component(const std::string& value)
|
||||||
|
{
|
||||||
|
std::ostringstream encoded;
|
||||||
|
encoded << std::uppercase << std::hex;
|
||||||
|
for (unsigned char c : value) {
|
||||||
|
if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
|
||||||
|
encoded << c;
|
||||||
|
else
|
||||||
|
encoded << '%' << std::setw(2) << std::setfill('0') << static_cast<unsigned int>(c);
|
||||||
|
}
|
||||||
|
return encoded.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string OrcaCloudSignalingChannel::host_without_scheme(std::string value)
|
||||||
|
{
|
||||||
|
const auto scheme = value.find("://");
|
||||||
|
if (scheme != std::string::npos)
|
||||||
|
value.erase(0, scheme + 3);
|
||||||
|
const auto slash = value.find('/');
|
||||||
|
if (slash != std::string::npos)
|
||||||
|
value.erase(slash);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OrcaCloudSignalingChannel::unavailable(CameraUnavailableReason reason, std::string detail)
|
||||||
|
{
|
||||||
|
if (on_unavailable)
|
||||||
|
on_unavailable(reason, std::move(detail));
|
||||||
|
}
|
||||||
|
|
||||||
|
void OrcaCloudSignalingChannel::run()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (!m_cloud || !m_cloud->ensure_token_fresh("camera")) {
|
||||||
|
unavailable(CameraUnavailableReason::Error, "Unable to refresh OrcaCloud credentials");
|
||||||
|
m_open.store(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::string token = m_cloud->get_access_token();
|
||||||
|
const std::string host = host_without_scheme(m_cloud->get_cloud_service_host());
|
||||||
|
if (token.empty() || host.empty()) {
|
||||||
|
unavailable(CameraUnavailableReason::Error, "OrcaCloud session is unavailable");
|
||||||
|
m_open.store(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string live_token_url =
|
||||||
|
"https://" + host + "/api/v1/printers/" + encode_path_component(m_dev_id) + "/live-token";
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "signaling: POST " << live_token_url << " (dev_id=" << m_dev_id << ")";
|
||||||
|
|
||||||
|
nlohmann::json token_response;
|
||||||
|
std::string token_body;
|
||||||
|
std::string token_error;
|
||||||
|
unsigned int http_code = 0;
|
||||||
|
auto request = Http::post(live_token_url);
|
||||||
|
request.set_post_body(std::string("{}"))
|
||||||
|
.header("Authorization", "Bearer " + token)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.tls_verify(true)
|
||||||
|
.timeout_max(30)
|
||||||
|
.on_complete([&token_body, &http_code](std::string body, unsigned status) {
|
||||||
|
http_code = status;
|
||||||
|
token_body = std::move(body);
|
||||||
|
})
|
||||||
|
.on_error([&token_body, &token_error, &http_code](std::string body, std::string error, unsigned status) {
|
||||||
|
http_code = status;
|
||||||
|
token_body = std::move(body);
|
||||||
|
token_error = std::move(error);
|
||||||
|
})
|
||||||
|
.perform_sync();
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "signaling: live-token HTTP " << http_code
|
||||||
|
<< (token_error.empty() ? "" : " error=" + token_error)
|
||||||
|
<< " body=" << token_body.substr(0, 512);
|
||||||
|
try {
|
||||||
|
token_response = nlohmann::json::parse(token_body);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "signaling: live-token body is not JSON: " << e.what();
|
||||||
|
}
|
||||||
|
if (http_code < 200 || http_code >= 300 || !token_response.contains("token")) {
|
||||||
|
unavailable(CameraUnavailableReason::Error, "Unable to mint camera live token");
|
||||||
|
m_open.store(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<CameraIceServer> ice_servers;
|
||||||
|
if (token_response.contains("ice_servers") && token_response["ice_servers"].is_array()) {
|
||||||
|
for (const auto& entry : token_response["ice_servers"]) {
|
||||||
|
if (entry.is_string()) {
|
||||||
|
ice_servers.push_back({entry.get<std::string>(), {}, {}});
|
||||||
|
} else if (entry.is_object()) {
|
||||||
|
// RTCIceServer.urls is "string | string[]" (Cloudflare
|
||||||
|
// Realtime returns an array). Emit one CameraIceServer per
|
||||||
|
// URL, sharing the credentials.
|
||||||
|
const std::string username = entry.value("username", std::string{});
|
||||||
|
const std::string credential = entry.value("credential", std::string{});
|
||||||
|
const auto add_url = [&](const nlohmann::json& url) {
|
||||||
|
if (url.is_string() && !url.get<std::string>().empty())
|
||||||
|
ice_servers.push_back({url.get<std::string>(), username, credential});
|
||||||
|
};
|
||||||
|
const auto urls = entry.find("urls");
|
||||||
|
if (urls != entry.end()) {
|
||||||
|
if (urls->is_array()) {
|
||||||
|
for (const auto& url : *urls)
|
||||||
|
add_url(url);
|
||||||
|
} else {
|
||||||
|
add_url(*urls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto conn = std::make_shared<Connection>();
|
||||||
|
conn->ssl_context.set_default_verify_paths();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_conn = conn;
|
||||||
|
}
|
||||||
|
auto& websocket = conn->websocket;
|
||||||
|
boost::asio::ip::tcp::resolver resolver(conn->io_context);
|
||||||
|
const auto endpoints = resolver.resolve(host, "443");
|
||||||
|
boost::asio::connect(boost::beast::get_lowest_layer(websocket), endpoints);
|
||||||
|
if (!SSL_set_tlsext_host_name(websocket.next_layer().native_handle(), host.c_str()))
|
||||||
|
throw std::runtime_error("Unable to configure TLS server name");
|
||||||
|
websocket.next_layer().set_verify_mode(boost::asio::ssl::verify_peer);
|
||||||
|
websocket.next_layer().handshake(boost::asio::ssl::stream_base::client);
|
||||||
|
const std::string ws_target = "/api/v1/printers/" + encode_path_component(m_dev_id) +
|
||||||
|
"/camera/live?token=" +
|
||||||
|
encode_path_component(token_response["token"].get<std::string>());
|
||||||
|
websocket.handshake(host, ws_target);
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "signaling: websocket handshake ok (" << ice_servers.size()
|
||||||
|
<< " ice servers)";
|
||||||
|
|
||||||
|
if (on_ready)
|
||||||
|
on_ready(std::move(ice_servers));
|
||||||
|
send_json(nlohmann::json{{"type", "camera.mode"}, {"mode", "webrtc"}}.dump());
|
||||||
|
// Async read loop, driven by the connection's own io_context. run()
|
||||||
|
// returns once close() has shut the socket down, giving a bounded,
|
||||||
|
// deadlock-free teardown from any thread.
|
||||||
|
do_read(conn);
|
||||||
|
conn->io_context.run();
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "signaling: run() exception: " << e.what();
|
||||||
|
if (!m_stop.load())
|
||||||
|
unavailable(CameraUnavailableReason::Closed, e.what());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
m_conn.reset();
|
||||||
|
}
|
||||||
|
m_open.store(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OrcaCloudSignalingChannel::do_read(std::shared_ptr<Connection> conn)
|
||||||
|
{
|
||||||
|
auto buffer = std::make_shared<boost::beast::flat_buffer>();
|
||||||
|
conn->websocket.async_read(
|
||||||
|
*buffer, [this, conn, buffer](boost::system::error_code ec, std::size_t) {
|
||||||
|
if (ec) {
|
||||||
|
if (!m_stop.load())
|
||||||
|
unavailable(CameraUnavailableReason::Closed, ec.message());
|
||||||
|
return; // do not re-arm; io_context.run() unwinds
|
||||||
|
}
|
||||||
|
const std::string raw = boost::beast::buffers_to_string(buffer->data());
|
||||||
|
// A malformed or unexpectedly-shaped message must not tear down the
|
||||||
|
// session: parse/dispatch is guarded.
|
||||||
|
try {
|
||||||
|
dispatch_message(nlohmann::json::parse(raw), raw);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "signaling: ignoring malformed message: " << e.what()
|
||||||
|
<< " raw=" << raw.substr(0, 256);
|
||||||
|
}
|
||||||
|
if (!m_stop.load())
|
||||||
|
do_read(conn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the string at key, or "" if absent or not a string (JSON null included).
|
||||||
|
static std::string json_string(const nlohmann::json& object, const char* key)
|
||||||
|
{
|
||||||
|
const auto it = object.find(key);
|
||||||
|
return (it != object.end() && it->is_string()) ? it->get<std::string>() : std::string{};
|
||||||
|
}
|
||||||
|
|
||||||
|
void OrcaCloudSignalingChannel::dispatch_message(const nlohmann::json& message, const std::string& raw)
|
||||||
|
{
|
||||||
|
const std::string type = json_string(message, "type");
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "signaling: recv type=" << type << " raw=" << raw.substr(0, 256);
|
||||||
|
if (type == "webrtc.answer") {
|
||||||
|
const std::string sdp = json_string(message, "sdp");
|
||||||
|
if (on_answer && !sdp.empty())
|
||||||
|
on_answer(sdp);
|
||||||
|
} else if (type == "webrtc.ice" && message.contains("candidate")) {
|
||||||
|
// The peer may send "candidate" as a flat string or as a nested
|
||||||
|
// RTCIceCandidateInit object { candidate, sdpMid, sdpMLineIndex }.
|
||||||
|
const nlohmann::json& candidate = message["candidate"];
|
||||||
|
std::string sdp_candidate;
|
||||||
|
std::string mid = json_string(message, "sdpMid");
|
||||||
|
if (candidate.is_string()) {
|
||||||
|
sdp_candidate = candidate.get<std::string>();
|
||||||
|
} else if (candidate.is_object()) {
|
||||||
|
sdp_candidate = json_string(candidate, "candidate");
|
||||||
|
std::string nested_mid = json_string(candidate, "sdpMid");
|
||||||
|
if (!nested_mid.empty())
|
||||||
|
mid = std::move(nested_mid);
|
||||||
|
}
|
||||||
|
if (on_ice && !sdp_candidate.empty())
|
||||||
|
on_ice(sdp_candidate, mid);
|
||||||
|
} else if (type == "webrtc.unavailable") {
|
||||||
|
const std::string reason = json_string(message, "reason");
|
||||||
|
unavailable(reason == "busy" ? CameraUnavailableReason::Busy
|
||||||
|
: reason == "disabled" ? CameraUnavailableReason::Disabled
|
||||||
|
: CameraUnavailableReason::Error,
|
||||||
|
reason.empty() ? "error" : reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OrcaCloudSignalingChannel::send_json(const std::string& message)
|
||||||
|
{
|
||||||
|
std::shared_ptr<Connection> conn;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
conn = m_conn;
|
||||||
|
}
|
||||||
|
if (!conn || m_stop.load())
|
||||||
|
return;
|
||||||
|
// Serialize the write onto the io_context thread (same thread that runs
|
||||||
|
// async_read), so reads and writes never touch the stream concurrently.
|
||||||
|
auto payload = std::make_shared<std::string>(message);
|
||||||
|
boost::asio::post(conn->io_context, [this, conn, payload] {
|
||||||
|
if (m_stop.load())
|
||||||
|
return;
|
||||||
|
boost::system::error_code ec;
|
||||||
|
conn->websocket.write(boost::asio::buffer(*payload), ec);
|
||||||
|
if (ec && !m_stop.load())
|
||||||
|
unavailable(CameraUnavailableReason::Closed, ec.message());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ICameraSignalingChannel.hpp"
|
||||||
|
#include "ICloudServiceAgent.hpp"
|
||||||
|
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/ip/tcp.hpp>
|
||||||
|
#include <boost/asio/ssl.hpp>
|
||||||
|
#include <boost/beast/ssl.hpp>
|
||||||
|
#include <boost/beast/websocket.hpp>
|
||||||
|
|
||||||
|
#include <nlohmann/json_fwd.hpp>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
class OrcaCloudSignalingChannel : public ICameraSignalingChannel {
|
||||||
|
public:
|
||||||
|
OrcaCloudSignalingChannel(std::shared_ptr<ICloudServiceAgent> cloud, std::string dev_id);
|
||||||
|
~OrcaCloudSignalingChannel() override;
|
||||||
|
|
||||||
|
void open() override;
|
||||||
|
void close() override;
|
||||||
|
void send_offer(std::string sdp) override;
|
||||||
|
void send_ice(std::string candidate, std::string mid) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
using WebSocket = boost::beast::websocket::stream<
|
||||||
|
boost::beast::ssl_stream<boost::asio::ip::tcp::socket>>;
|
||||||
|
|
||||||
|
// The io_context and ssl_context must outlive the websocket stream that
|
||||||
|
// references them. Bundling them here with the stream declared last makes
|
||||||
|
// the destruction order correct (stream first, then contexts), and lets a
|
||||||
|
// single shared_ptr own the whole set.
|
||||||
|
struct Connection {
|
||||||
|
boost::asio::io_context io_context;
|
||||||
|
boost::asio::ssl::context ssl_context{boost::asio::ssl::context::tls_client};
|
||||||
|
WebSocket websocket{io_context, ssl_context};
|
||||||
|
};
|
||||||
|
|
||||||
|
void run();
|
||||||
|
void do_read(std::shared_ptr<Connection> conn);
|
||||||
|
void dispatch_message(const nlohmann::json& message, const std::string& raw);
|
||||||
|
void send_json(const std::string& message);
|
||||||
|
void unavailable(CameraUnavailableReason reason, std::string detail);
|
||||||
|
static std::string encode_path_component(const std::string& value);
|
||||||
|
static std::string host_without_scheme(std::string value);
|
||||||
|
|
||||||
|
std::shared_ptr<ICloudServiceAgent> m_cloud;
|
||||||
|
std::string m_dev_id;
|
||||||
|
std::atomic<bool> m_stop{false};
|
||||||
|
std::atomic<bool> m_open{false};
|
||||||
|
std::thread m_thread;
|
||||||
|
mutable std::mutex m_mutex;
|
||||||
|
std::shared_ptr<Connection> m_conn;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "OrcaPrinterAgent.hpp"
|
#include "OrcaPrinterAgent.hpp"
|
||||||
|
#include "OrcaCloudSignalingChannel.hpp"
|
||||||
#include "Http.hpp"
|
#include "Http.hpp"
|
||||||
#include "IPrinterAgent.hpp"
|
#include "IPrinterAgent.hpp"
|
||||||
#include "NetworkAgentFactory.hpp"
|
#include "NetworkAgentFactory.hpp"
|
||||||
@@ -638,6 +639,15 @@ void OrcaPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud
|
|||||||
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_cloud_agent: status callback result=" << callback_result;
|
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::set_cloud_agent: status callback result=" << callback_result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<ICameraSignalingChannel>
|
||||||
|
OrcaPrinterAgent::create_camera_signaling_channel(const std::string& dev_id)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
|
if (!m_cloud_agent)
|
||||||
|
return nullptr;
|
||||||
|
return std::make_unique<OrcaCloudSignalingChannel>(m_cloud_agent, dev_id);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Communication
|
// Communication
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ public:
|
|||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) override;
|
void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) override;
|
||||||
|
CameraStreamMode get_camera_stream_mode() const override;
|
||||||
|
std::string get_camera_url() const override;
|
||||||
|
std::unique_ptr<ICameraSignalingChannel>
|
||||||
|
create_camera_signaling_channel(const std::string& dev_id) override;
|
||||||
|
|
||||||
// Communication
|
// Communication
|
||||||
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
|
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
|
||||||
@@ -63,9 +67,6 @@ public:
|
|||||||
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override;
|
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override;
|
||||||
int set_server_callback(OnServerErrFn fn) override;
|
int set_server_callback(OnServerErrFn fn) override;
|
||||||
|
|
||||||
CameraStreamMode get_camera_stream_mode() const override;
|
|
||||||
std::string get_camera_url() const override;
|
|
||||||
|
|
||||||
// Machine Selection
|
// Machine Selection
|
||||||
std::string get_user_selected_machine() override;
|
std::string get_user_selected_machine() override;
|
||||||
int set_user_selected_machine(std::string dev_id) override;
|
int set_user_selected_machine(std::string dev_id) override;
|
||||||
|
|||||||
Reference in New Issue
Block a user