recorder-for-jetkvm 0.1.0

JetKVM recorder and screenshot utility
Documentation
use anyhow::{Context, Result};
use base64::prelude::*;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tracing::debug;
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;

use crate::auth;

#[derive(Serialize, Deserialize)]
struct SdpMessage {
    #[serde(rename = "type")]
    sdp_type: String,
    sdp: String,
}

#[derive(Serialize)]
struct SessionRequest {
    sd: String,
}

#[derive(Deserialize)]
struct SessionResponse {
    sd: String,
}

fn encode_sdp(desc: &RTCSessionDescription) -> Result<String> {
    let msg = SdpMessage {
        sdp_type: desc.sdp_type.to_string().to_lowercase(),
        sdp: desc.sdp.clone(),
    };
    let json = serde_json::to_string(&msg).context("failed to serialize SDP")?;
    Ok(BASE64_STANDARD.encode(json.as_bytes()))
}

fn decode_sdp(encoded: &str) -> Result<RTCSessionDescription> {
    let json_bytes = BASE64_STANDARD
        .decode(encoded)
        .context("failed to decode base64 SDP")?;
    let msg: SdpMessage =
        serde_json::from_slice(&json_bytes).context("failed to parse SDP JSON")?;
    debug!(sdp_type = %msg.sdp_type, "decoded remote SDP");
    match msg.sdp_type.as_str() {
        "answer" => RTCSessionDescription::answer(msg.sdp).context("failed to parse SDP answer"),
        "offer" => RTCSessionDescription::offer(msg.sdp).context("failed to parse SDP offer"),
        other => anyhow::bail!("unknown SDP type: {other}"),
    }
}

pub async fn exchange_sdp(
    client: &Client,
    host: &str,
    offer: &RTCSessionDescription,
) -> Result<RTCSessionDescription> {
    let encoded_offer = encode_sdp(offer)?;
    let base = auth::base_url(host);
    let url = format!("{base}/webrtc/session");

    debug!("sending SDP offer to {url}");
    let resp = client
        .post(&url)
        .json(&SessionRequest { sd: encoded_offer })
        .send()
        .await
        .context("failed to send SDP offer")?;

    let status = resp.status();
    if !status.is_success() {
        let body = resp.text().await.unwrap_or_default();
        anyhow::bail!("signaling failed (HTTP {status}): {body}");
    }

    let session_resp: SessionResponse = resp
        .json()
        .await
        .context("failed to parse signaling response")?;

    let answer = decode_sdp(&session_resp.sd)?;
    Ok(answer)
}