use clutchcall::{voice, streams, robotics, games, data, moqt};
async and use tokio. They share the same
underlying client (clutchcall::Client) when constructed against the
same relay.
Voice
let v = voice::Voice::new(voice::Config {
base_url: "https://app.clutchcall.dev".into(),
api_key: std::env::var("CLUTCHCALL_API_KEY")?,
org_id: Some("org_abc".into()),
})?;
Calls
let call = v.calls().originate(voice::OriginateArgs {
to: "+15551234567",
from: Some("+15558675309"),
trunk_id: Some("trunk_main"),
agent: Some("healthcare-assistant"),
}).await?;
v.calls().get(sid).await?;
v.calls().list(org_id, cursor, limit).await?;
v.calls().transfer(sid, voice::TransferArgs { to: "...", agent: "..." }).await?;
v.calls().hangup(sid).await?;
v.calls().terminate(sid).await?;
Call:
call.sid: String
call.status: String
call.from: String
call.to: String
call.refresh().await? -> CallData
call.hangup().await?
call.transfer(args).await? -> Call
call.on_status(cb) -> Unsubscribe
AudioBridge
let bridge = v.audio_bridge().attach(call.sid, voice::AudioBridgeOpts {
codec: voice::Codec::Opus,
on_uplink: Some(Box::new(|frame, ts_us| { /* ... */ })),
})?;
bridge.publish_uplink(frame);
bridge.publish_downlink(frame);
bridge.on_uplink(cb);
bridge.on_downlink(cb);
bridge.close();
Codec: Opus, Pcm16, G711ULaw, G711ALaw.
Streams
let s = streams::Streams::new(streams::Config {
base_url: "https://app.clutchcall.dev".into(),
api_key: key,
org_id: "org_abc".into(),
})?;
Control plane
let (input, stream_key) = s.live_inputs().create(streams::CreateArgs {
name: "My Show".into(), codecs: None, recording_profile: None,
}).await?; // stream_key returned ONCE
s.live_inputs().get(id).await?;
s.live_inputs().list(org_id, cursor, limit).await?;
s.live_inputs().rotate_stream_key(id).await?;
s.live_inputs().delete(id).await?;
s.signing_keys().create(org_id, label).await?;
s.signing_keys().list(org_id).await?;
s.signing_keys().retire(id).await?;
s.api_keys().create(org_id, label, scopes).await?;
s.api_keys().list(org_id).await?;
s.api_keys().revoke(id).await?;
s.webhooks().create(org_id, url, events).await?;
s.webhooks().list(org_id).await?;
s.webhooks().delete(id).await?;
s.events().list_deliveries(webhook_id, cursor, limit).await?;
s.analytics().viewer_minutes(org_id, from, to, group_by).await?;
s.analytics().pop_cache(org_id, from, to).await?;
LiveInput:
input.external_input_id: String
input.signed_playback_url(ttl_seconds, scopes).await? -> SignedPlaybackUrl
Data plane
let pub_ = streams::BroadcastPublisher::open(streams::PublisherArgs {
input_id, stream_key, codecs,
}).await?;
pub_.write(chunk);
pub_.close(reason).await?;
let viewer = streams::BroadcastViewer::open(signed_url, streams::ViewerOpts {
on_chunk: Box::new(|init, chunk| { /* ... */ }),
on_close: Box::new(|reason| { /* ... */ }),
}).await?;
viewer.close().await?;
Robotics
let r = robotics::Robotics::new(robotics::Config {
relay_host: "relay.clutchcall.dev".into(),
token: std::env::var("CLUTCHCALL_RELAY_TOKEN")?,
robot_id: "turtlebot-7".into(),
})?;
let pub_ = r.publish_telemetry(robotics::PublishArgs {
topic: "odom",
type_name: "nav_msgs/msg/Odometry",
qos: Some(robotics::QoSProfile {
reliability: robotics::Reliability::Reliable,
depth: Some(10),
..Default::default()
}),
}).await?;
pub_.write(cdr_bytes);
pub_.close();
let sub = r.subscribe_telemetry(
robotics::SubscribeArgs { topic: "odom", qos: None },
|payload, type_name| { /* ... */ },
).await?;
sub.close();
QoSProfile:
pub struct QoSProfile {
pub reliability: Reliability, // BestEffort | Reliable
pub durability: Durability, // Volatile | TransientLocal
pub depth: Option<u32>,
}
Games
let g = games::Games::new(games::Config {
relay_host: "relay.clutchcall.dev".into(),
token,
room_id: "duel-42".into(),
player_id: Some("alice".into()), // None = authority
})?;
Player
let sub = g.subscribe_state(|bytes| render(bytes)).await?;
let input = g.publish_input().await?;
input.write(serialize_input(local_input));
let events = g.publish_event().await?;
events.send(serde_json::json!({"kind": "chat", "text": "gg"}));
g.subscribe_events(|evt, from_player_id| dispatch(evt)).await?;
Authority (player_id: None)
let state = g.publish_state(games::StateOpts { tick_hz: 30 }).await?;
state.write(serialize_state(&world));
g.subscribe_inputs(|player_id, payload| apply_input(player_id, payload)).await?;
g.subscribe_events(|evt, from_player_id| { /* ... */ }).await?;
Data
let d = data::Data::new(data::Config {
relay_host: "relay.clutchcall.dev".into(),
token: std::env::var("CLUTCHCALL_DATA_TOKEN")?,
client_id: "device-7".into(),
})?;
d.publish(data::PublishArgs {
topic: "sensors/room1/temperature",
payload: b"23.5".to_vec(),
reliable: false,
retain: false,
}).await?;
let sub = d.subscribe(data::SubscribeArgs {
topic_filter: "sensors/+/temperature".into(),
}, |msg: data::Message| {
println!("{} ← {} = {:?}", msg.topic, msg.from_client_id, msg.payload);
}).await?;
sub.close();
Message:
pub struct Message {
pub topic: String,
pub payload: Vec<u8>,
pub from_client_id: String,
pub retain: bool,
pub ts_us: u64,
}
Realtime Tracks
See Realtime Tracks. Common methods:let client = moqt::MoqtClient::connect("quic://relay.clutchcall.dev", token).await?;
client.publish_frame(namespace, name, opts).await?;
client.subscribe_frame(namespace, name, |ts_us, prio, data| { /* ... */ }).await?;
client.publish_audio(...) / client.subscribe_audio(...)
client.publish_video(...) / client.subscribe_video(...)
client.publish_text(...) / client.subscribe_text(...)
client.subscribe_namespace(prefix, |suffix, active| { /* ... */ }).await?;
client.connection_rtt_us() -> u64
client.max_datagram_size() -> usize
client.close();
Legacy RPC
The rootclutchcall::Client (dial, originate_bulk, hangup,
barge, push_audio, …) remains for backwards compat. New code should
prefer the voice modality.
