mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-11 00:05:36 +02:00
## Description ### First commit Restructured protobuf schemas to make them easier to use across languages, switched to using them in-place of JSON for signaling as well, so there's no 2 different message formats flying about. Few new message types to deal with clients and nestri-servers better (not final format, may see changes still). General cleanup of dead/unused code along some bug squashing and package updates. TODO for future commits: - [x] Fix additional controllers not doing inputs (possibly needs vimputti changes) - [x] ~~Restructure relay protocols code a bit, to reduce bloatiness of the currently single file for them, more code re-use.~~ - Gonna keep this PR somewhat manageable without poking more at relay.. - [x] ~~Try to fix issue where with multiple clients, static stream content causes video to freeze until there's some movement.~~ - Was caused by server tuned profile being `throughput-performance`, causing CPU latency to be too high. - [x] Ponder the orb ### Second + third commit Redid the controller polling handling and fixed multi-controller handling in vimputti and nestri code sides. Remove some dead relay code as well to clean up the protocol source file, we'll revisit the meshing functionality later. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added software rendering option and MangoHud runtime config; controller sessions now support reconnection and batched state updates with persistent session IDs. * **Bug Fixes** * Restored previously-filtered NES-like gamepads so they connect correctly. * **Chores** * Modernized dependencies and protobuf tooling, migrated to protobuf-based messaging and streaming, and removed obsolete CUDA build steps. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
538 lines
21 KiB
Rust
538 lines
21 KiB
Rust
use crate::input::controller::ControllerManager;
|
|
use crate::p2p::p2p::NestriConnection;
|
|
use crate::p2p::p2p_protocol_stream::NestriStreamProtocol;
|
|
use crate::proto::proto::proto_message::Payload;
|
|
use crate::proto::proto::{
|
|
ProtoControllerAttach, ProtoControllerRumble, ProtoIce, ProtoMessage, ProtoSdp,
|
|
ProtoServerPushStream, RtcIceCandidateInit, RtcSessionDescriptionInit,
|
|
};
|
|
use anyhow::Result;
|
|
use glib::subclass::prelude::*;
|
|
use gstreamer::glib;
|
|
use gstreamer::prelude::*;
|
|
use gstreamer_webrtc::{WebRTCSDPType, WebRTCSessionDescription, gst_sdp};
|
|
use gstrswebrtc::signaller::{Signallable, SignallableImpl};
|
|
use parking_lot::RwLock as PLRwLock;
|
|
use prost::Message;
|
|
use std::sync::{Arc, LazyLock};
|
|
use tokio::sync::{Mutex, mpsc};
|
|
|
|
pub struct Signaller {
|
|
stream_room: PLRwLock<Option<String>>,
|
|
stream_protocol: PLRwLock<Option<Arc<NestriStreamProtocol>>>,
|
|
wayland_src: PLRwLock<Option<Arc<gstreamer::Element>>>,
|
|
data_channel: PLRwLock<Option<Arc<gstreamer_webrtc::WebRTCDataChannel>>>,
|
|
controller_manager: PLRwLock<Option<Arc<ControllerManager>>>,
|
|
rumble_rx: Mutex<Option<mpsc::Receiver<(u32, u16, u16, u16, String)>>>,
|
|
attach_rx: Mutex<Option<mpsc::Receiver<ProtoControllerAttach>>>,
|
|
}
|
|
impl Default for Signaller {
|
|
fn default() -> Self {
|
|
Self {
|
|
stream_room: PLRwLock::new(None),
|
|
stream_protocol: PLRwLock::new(None),
|
|
wayland_src: PLRwLock::new(None),
|
|
data_channel: PLRwLock::new(None),
|
|
controller_manager: PLRwLock::new(None),
|
|
rumble_rx: Mutex::new(None),
|
|
attach_rx: Mutex::new(None),
|
|
}
|
|
}
|
|
}
|
|
impl Signaller {
|
|
pub async fn set_nestri_connection(&self, nestri_conn: NestriConnection) -> Result<()> {
|
|
let stream_protocol = NestriStreamProtocol::new(nestri_conn).await?;
|
|
*self.stream_protocol.write() = Some(Arc::new(stream_protocol));
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_stream_room(&self, room: String) {
|
|
*self.stream_room.write() = Some(room);
|
|
}
|
|
|
|
fn get_stream_protocol(&self) -> Option<Arc<NestriStreamProtocol>> {
|
|
self.stream_protocol.read().clone()
|
|
}
|
|
|
|
pub fn set_wayland_src(&self, wayland_src: Arc<gstreamer::Element>) {
|
|
*self.wayland_src.write() = Some(wayland_src);
|
|
}
|
|
|
|
pub fn get_wayland_src(&self) -> Option<Arc<gstreamer::Element>> {
|
|
self.wayland_src.read().clone()
|
|
}
|
|
|
|
pub fn set_controller_manager(&self, controller_manager: Arc<ControllerManager>) {
|
|
*self.controller_manager.write() = Some(controller_manager);
|
|
}
|
|
|
|
pub fn get_controller_manager(&self) -> Option<Arc<ControllerManager>> {
|
|
self.controller_manager.read().clone()
|
|
}
|
|
|
|
pub async fn set_rumble_rx(&self, rumble_rx: mpsc::Receiver<(u32, u16, u16, u16, String)>) {
|
|
*self.rumble_rx.lock().await = Some(rumble_rx);
|
|
}
|
|
|
|
pub async fn take_rumble_rx(&self) -> Option<mpsc::Receiver<(u32, u16, u16, u16, String)>> {
|
|
self.rumble_rx.lock().await.take()
|
|
}
|
|
|
|
pub async fn set_attach_rx(
|
|
&self,
|
|
attach_rx: mpsc::Receiver<crate::proto::proto::ProtoControllerAttach>,
|
|
) {
|
|
*self.attach_rx.lock().await = Some(attach_rx);
|
|
}
|
|
|
|
pub async fn take_attach_rx(
|
|
&self,
|
|
) -> Option<mpsc::Receiver<crate::proto::proto::ProtoControllerAttach>> {
|
|
self.attach_rx.lock().await.take()
|
|
}
|
|
|
|
pub fn set_data_channel(&self, data_channel: gstreamer_webrtc::WebRTCDataChannel) {
|
|
*self.data_channel.write() = Some(Arc::new(data_channel));
|
|
}
|
|
|
|
pub fn get_data_channel(&self) -> Option<Arc<gstreamer_webrtc::WebRTCDataChannel>> {
|
|
self.data_channel.read().clone()
|
|
}
|
|
|
|
/// Helper method to clean things up
|
|
fn register_callbacks(&self) {
|
|
let Some(stream_protocol) = self.get_stream_protocol() else {
|
|
gstreamer::error!(gstreamer::CAT_DEFAULT, "Stream protocol not set");
|
|
return;
|
|
};
|
|
{
|
|
let self_obj = self.obj().clone();
|
|
stream_protocol.register_callback("answer", move |msg| {
|
|
if let Some(payload) = msg.payload {
|
|
match payload {
|
|
Payload::Sdp(sdp) => {
|
|
if let Some(sdp) = sdp.sdp {
|
|
let sdp = gst_sdp::SDPMessage::parse_buffer(sdp.sdp.as_bytes())
|
|
.map_err(|e| {
|
|
anyhow::anyhow!("Invalid SDP in 'answer': {e:?}")
|
|
})?;
|
|
let answer =
|
|
WebRTCSessionDescription::new(WebRTCSDPType::Answer, sdp);
|
|
return Ok(self_obj.emit_by_name::<()>(
|
|
"session-description",
|
|
&[&"unique-session-id", &answer],
|
|
));
|
|
}
|
|
}
|
|
_ => {
|
|
tracing::warn!("Unexpected payload type for answer");
|
|
return Ok(());
|
|
}
|
|
}
|
|
} else {
|
|
anyhow::bail!("Failed to decode answer message");
|
|
}
|
|
Ok(())
|
|
});
|
|
}
|
|
{
|
|
let self_obj = self.obj().clone();
|
|
stream_protocol.register_callback("ice-candidate", move |msg| {
|
|
if let Some(payload) = msg.payload {
|
|
match payload {
|
|
Payload::Ice(ice) => {
|
|
if let Some(candidate) = ice.candidate {
|
|
let sdp_m_line_index = candidate.sdp_m_line_index.unwrap_or(0);
|
|
return Ok(self_obj.emit_by_name::<()>(
|
|
"handle-ice",
|
|
&[
|
|
&"unique-session-id",
|
|
&sdp_m_line_index,
|
|
&candidate.sdp_mid,
|
|
&candidate.candidate,
|
|
],
|
|
));
|
|
}
|
|
}
|
|
_ => {
|
|
tracing::warn!("Unexpected payload type for ice-candidate");
|
|
return Ok(());
|
|
}
|
|
}
|
|
} else {
|
|
anyhow::bail!("Failed to decode ICE message");
|
|
}
|
|
Ok(())
|
|
});
|
|
}
|
|
{
|
|
let self_obj = self.obj().clone();
|
|
stream_protocol.register_callback("push-stream-ok", move |msg| {
|
|
if let Some(payload) = msg.payload {
|
|
return match payload {
|
|
Payload::ServerPushStream(_res) => {
|
|
// Send our SDP offer
|
|
Ok(self_obj.emit_by_name::<()>(
|
|
"session-requested",
|
|
&[
|
|
&"unique-session-id",
|
|
&"consumer-identifier",
|
|
&None::<WebRTCSessionDescription>,
|
|
],
|
|
))
|
|
}
|
|
_ => {
|
|
tracing::warn!("Unexpected payload type for push-stream-ok");
|
|
Ok(())
|
|
}
|
|
};
|
|
} else {
|
|
anyhow::bail!("Failed to decode answer");
|
|
}
|
|
});
|
|
}
|
|
{
|
|
let self_obj = self.obj().clone();
|
|
// After creating webrtcsink
|
|
self_obj.connect_closure(
|
|
"webrtcbin-ready",
|
|
false,
|
|
glib::closure!(
|
|
move |signaller: &super::NestriSignaller,
|
|
_consumer_identifier: &str,
|
|
webrtcbin: &gstreamer::Element| {
|
|
gstreamer::info!(gstreamer::CAT_DEFAULT, "Adding data channels");
|
|
// Create data channels on webrtcbin
|
|
let data_channel = Some(
|
|
webrtcbin.emit_by_name::<gstreamer_webrtc::WebRTCDataChannel>(
|
|
"create-data-channel",
|
|
&[
|
|
&"nestri-data-channel",
|
|
&gstreamer::Structure::builder("config")
|
|
.field("ordered", &true)
|
|
.field("max-retransmits", &2u32)
|
|
.field("priority", "high")
|
|
.field("protocol", "raw")
|
|
.build(),
|
|
],
|
|
),
|
|
);
|
|
if let Some(data_channel) = data_channel {
|
|
gstreamer::info!(gstreamer::CAT_DEFAULT, "Data channel created");
|
|
if let Some(wayland_src) = signaller.imp().get_wayland_src() {
|
|
signaller.imp().set_data_channel(data_channel.clone());
|
|
|
|
let signaller = signaller.clone();
|
|
let data_channel = Arc::new(data_channel);
|
|
let wayland_src = wayland_src.clone();
|
|
|
|
// Spawn async task to take the receiver and set up
|
|
tokio::spawn(async move {
|
|
let rumble_rx = signaller.imp().take_rumble_rx().await;
|
|
let attach_rx = signaller.imp().take_attach_rx().await;
|
|
let controller_manager =
|
|
signaller.imp().get_controller_manager();
|
|
|
|
setup_data_channel(
|
|
controller_manager,
|
|
rumble_rx,
|
|
attach_rx,
|
|
data_channel,
|
|
&wayland_src,
|
|
);
|
|
});
|
|
} else {
|
|
gstreamer::error!(
|
|
gstreamer::CAT_DEFAULT,
|
|
"Wayland display source not set"
|
|
);
|
|
}
|
|
} else {
|
|
gstreamer::error!(
|
|
gstreamer::CAT_DEFAULT,
|
|
"Failed to create data channel"
|
|
);
|
|
}
|
|
}
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
impl SignallableImpl for Signaller {
|
|
fn start(&self) {
|
|
gstreamer::info!(gstreamer::CAT_DEFAULT, "Signaller started");
|
|
|
|
// Register message callbacks
|
|
self.register_callbacks();
|
|
|
|
// Subscribe to reconnection notifications
|
|
// TODO: Re-implement reconnection handling
|
|
|
|
let Some(stream_room) = self.stream_room.read().clone() else {
|
|
gstreamer::error!(gstreamer::CAT_DEFAULT, "Stream room not set");
|
|
return;
|
|
};
|
|
|
|
let Some(stream_protocol) = self.get_stream_protocol() else {
|
|
gstreamer::error!(gstreamer::CAT_DEFAULT, "Stream protocol not set");
|
|
return;
|
|
};
|
|
|
|
let push_msg = crate::proto::create_message(
|
|
Payload::ServerPushStream(ProtoServerPushStream {
|
|
room_name: stream_room,
|
|
}),
|
|
"push-stream-room",
|
|
None,
|
|
);
|
|
if let Err(e) = stream_protocol.send_message(&push_msg) {
|
|
tracing::error!("Failed to send push stream room message: {:?}", e);
|
|
}
|
|
}
|
|
|
|
fn stop(&self) {
|
|
gstreamer::info!(gstreamer::CAT_DEFAULT, "Signaller stopped");
|
|
}
|
|
|
|
fn send_sdp(&self, _session_id: &str, sdp: &WebRTCSessionDescription) {
|
|
let Some(stream_protocol) = self.get_stream_protocol() else {
|
|
gstreamer::error!(gstreamer::CAT_DEFAULT, "Stream protocol not set");
|
|
return;
|
|
};
|
|
|
|
let sdp_msg = crate::proto::create_message(
|
|
Payload::Sdp(ProtoSdp {
|
|
sdp: Some(RtcSessionDescriptionInit {
|
|
sdp: sdp.sdp().as_text().unwrap(),
|
|
r#type: "offer".to_string(),
|
|
}),
|
|
}),
|
|
"offer",
|
|
None,
|
|
);
|
|
if let Err(e) = stream_protocol.send_message(&sdp_msg) {
|
|
tracing::error!("Failed to send SDP message: {:?}", e);
|
|
}
|
|
}
|
|
|
|
fn add_ice(
|
|
&self,
|
|
_session_id: &str,
|
|
candidate: &str,
|
|
sdp_m_line_index: u32,
|
|
sdp_mid: Option<String>,
|
|
) {
|
|
let Some(stream_protocol) = self.get_stream_protocol() else {
|
|
gstreamer::error!(gstreamer::CAT_DEFAULT, "Stream protocol not set");
|
|
return;
|
|
};
|
|
|
|
let candidate_init = RtcIceCandidateInit {
|
|
candidate: candidate.to_string(),
|
|
sdp_mid,
|
|
sdp_m_line_index: Some(sdp_m_line_index),
|
|
..Default::default() //username_fragment: Some(session_id.to_string()), TODO: required?
|
|
};
|
|
let ice_msg = crate::proto::create_message(
|
|
Payload::Ice(ProtoIce {
|
|
candidate: Some(candidate_init),
|
|
}),
|
|
"ice-candidate",
|
|
None,
|
|
);
|
|
if let Err(e) = stream_protocol.send_message(&ice_msg) {
|
|
tracing::error!("Failed to send ICE candidate message: {:?}", e);
|
|
}
|
|
}
|
|
|
|
fn end_session(&self, session_id: &str) {
|
|
gstreamer::info!(gstreamer::CAT_DEFAULT, "Ending session: {}", session_id);
|
|
}
|
|
}
|
|
#[glib::object_subclass]
|
|
impl ObjectSubclass for Signaller {
|
|
const NAME: &'static str = "NestriSignaller";
|
|
type Type = super::NestriSignaller;
|
|
type ParentType = glib::Object;
|
|
type Interfaces = (Signallable,);
|
|
}
|
|
impl ObjectImpl for Signaller {
|
|
fn properties() -> &'static [glib::ParamSpec] {
|
|
static PROPS: LazyLock<Vec<glib::ParamSpec>> = LazyLock::new(|| {
|
|
vec![
|
|
glib::ParamSpecBoolean::builder("manual-sdp-munging")
|
|
.nick("Manual SDP munging")
|
|
.blurb("Whether the signaller manages SDP munging itself")
|
|
.default_value(false)
|
|
.read_only()
|
|
.build(),
|
|
]
|
|
});
|
|
|
|
PROPS.as_ref()
|
|
}
|
|
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
|
match pspec.name() {
|
|
"manual-sdp-munging" => false.to_value(),
|
|
_ => unimplemented!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn setup_data_channel(
|
|
controller_manager: Option<Arc<ControllerManager>>,
|
|
rumble_rx: Option<mpsc::Receiver<(u32, u16, u16, u16, String)>>, // (slot, strong, weak, duration_ms, session_id)
|
|
attach_rx: Option<mpsc::Receiver<ProtoControllerAttach>>,
|
|
data_channel: Arc<gstreamer_webrtc::WebRTCDataChannel>,
|
|
wayland_src: &gstreamer::Element,
|
|
) {
|
|
let wayland_src = wayland_src.clone();
|
|
let (tx, mut rx) = mpsc::unbounded_channel::<Vec<u8>>();
|
|
|
|
// Spawn async processor
|
|
tokio::spawn(async move {
|
|
while let Some(data) = rx.recv().await {
|
|
match ProtoMessage::decode(data.as_slice()) {
|
|
Ok(msg_wrapper) => {
|
|
if let Some(message_base) = msg_wrapper.message_base {
|
|
if message_base.payload_type == "input" {
|
|
if let Some(input_data) = msg_wrapper.payload {
|
|
if let Some(event) = handle_input_message(input_data) {
|
|
// Send the event to wayland source, result bool is ignored
|
|
let _ = wayland_src.send_event(event);
|
|
}
|
|
}
|
|
} else if message_base.payload_type == "controllerInput" {
|
|
if let Some(controller_manager) = &controller_manager {
|
|
if let Some(input_data) = msg_wrapper.payload {
|
|
let _ = controller_manager.send_command(input_data).await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to decode input message: {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Spawn rumble sender
|
|
if let Some(mut rumble_rx) = rumble_rx {
|
|
let data_channel_clone = data_channel.clone();
|
|
tokio::spawn(async move {
|
|
while let Some((slot, strong, weak, duration_ms, session_id)) = rumble_rx.recv().await {
|
|
let rumble_msg = crate::proto::create_message(
|
|
Payload::ControllerRumble(ProtoControllerRumble {
|
|
session_slot: slot as i32,
|
|
session_id: session_id,
|
|
low_frequency: weak as i32,
|
|
high_frequency: strong as i32,
|
|
duration: duration_ms as i32,
|
|
}),
|
|
"controllerInput",
|
|
None,
|
|
);
|
|
|
|
let data = rumble_msg.encode_to_vec();
|
|
let bytes = glib::Bytes::from_owned(data);
|
|
|
|
if let Err(e) = data_channel_clone.send_data_full(Some(&bytes)) {
|
|
tracing::warn!("Failed to send rumble data: {}", e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Spawn attach sender
|
|
if let Some(mut attach_rx) = attach_rx {
|
|
let data_channel_clone = data_channel.clone();
|
|
tokio::spawn(async move {
|
|
while let Some(attach_msg) = attach_rx.recv().await {
|
|
let proto_msg = crate::proto::create_message(
|
|
Payload::ControllerAttach(attach_msg),
|
|
"controllerInput",
|
|
None,
|
|
);
|
|
|
|
let data = proto_msg.encode_to_vec();
|
|
let bytes = glib::Bytes::from_owned(data);
|
|
|
|
if let Err(e) = data_channel_clone.send_data_full(Some(&bytes)) {
|
|
tracing::warn!("Failed to send controller attach data: {}", e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
data_channel.connect_on_message_data(move |_data_channel, data| {
|
|
if let Some(data) = data {
|
|
let _ = tx.send(data.to_vec());
|
|
}
|
|
});
|
|
}
|
|
|
|
fn handle_input_message(payload: Payload) -> Option<gstreamer::Event> {
|
|
match payload {
|
|
Payload::MouseMove(data) => {
|
|
let structure = gstreamer::Structure::builder("MouseMoveRelative")
|
|
.field("pointer_x", data.x as f64)
|
|
.field("pointer_y", data.y as f64)
|
|
.build();
|
|
|
|
Some(gstreamer::event::CustomUpstream::new(structure))
|
|
}
|
|
Payload::MouseMoveAbs(data) => {
|
|
let structure = gstreamer::Structure::builder("MouseMoveAbsolute")
|
|
.field("pointer_x", data.x as f64)
|
|
.field("pointer_y", data.y as f64)
|
|
.build();
|
|
|
|
Some(gstreamer::event::CustomUpstream::new(structure))
|
|
}
|
|
Payload::KeyDown(data) => {
|
|
let structure = gstreamer::Structure::builder("KeyboardKey")
|
|
.field("key", data.key as u32)
|
|
.field("pressed", true)
|
|
.build();
|
|
|
|
Some(gstreamer::event::CustomUpstream::new(structure))
|
|
}
|
|
Payload::KeyUp(data) => {
|
|
let structure = gstreamer::Structure::builder("KeyboardKey")
|
|
.field("key", data.key as u32)
|
|
.field("pressed", false)
|
|
.build();
|
|
|
|
Some(gstreamer::event::CustomUpstream::new(structure))
|
|
}
|
|
Payload::MouseWheel(data) => {
|
|
let structure = gstreamer::Structure::builder("MouseAxis")
|
|
.field("x", data.x as f64)
|
|
.field("y", data.y as f64)
|
|
.build();
|
|
|
|
Some(gstreamer::event::CustomUpstream::new(structure))
|
|
}
|
|
Payload::MouseKeyDown(data) => {
|
|
let structure = gstreamer::Structure::builder("MouseButton")
|
|
.field("button", data.key as u32)
|
|
.field("pressed", true)
|
|
.build();
|
|
|
|
Some(gstreamer::event::CustomUpstream::new(structure))
|
|
}
|
|
Payload::MouseKeyUp(data) => {
|
|
let structure = gstreamer::Structure::builder("MouseButton")
|
|
.field("button", data.key as u32)
|
|
.field("pressed", false)
|
|
.build();
|
|
|
|
Some(gstreamer::event::CustomUpstream::new(structure))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|