breadcast/breadcast-caststream-sys/tests/smoke.rs
Breadway 8c745d18e0
Some checks failed
dev release / build (push) Failing after 12s
Implement Cast Streaming mirroring, DLNA casting, daemon+GUI, and breadd integration
Builds out the full v1 scope: a vendored+patched openscreen subset for
low-latency Cast Streaming (Mirroring receiver 0F5096E8) alongside the
existing Cast V2/HLS and new DLNA/AVTransport casting paths, breadcastd's
Idle/Casting state machine with a private IPC socket, the breadcast GTK4
popup as a thin IPC client, and bread.cast.*/bread.command.cast.* breadd
integration (device discovery, start/stop, mirroring lifecycle events).
Also adds bakery/systemd/Forgejo CI packaging.

Validated end-to-end against a real Chromecast/Google TV: negotiated
Cast Streaming session, live pipeline playback, and daemon+GUI click-to-cast/
stop through the actual popup.
2026-08-03 09:07:21 +08:00

91 lines
3.4 KiB
Rust

//! Exercises the raw FFI end-to-end from Rust: create a sender, kick off
//! negotiation, and confirm the C++ side calls back out through
//! `post_message` with a real Cast Streaming OFFER -- without needing an
//! actual receiver on the network (nothing here waits for an ANSWER).
use std::ffi::c_void;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use std::time::{Duration, Instant};
use breadcast_caststream_sys::{
breadcast_caststream_sender_create, breadcast_caststream_sender_destroy,
breadcast_caststream_sender_estimated_bandwidth_bps,
breadcast_caststream_sender_needs_key_frame, breadcast_caststream_sender_negotiate,
};
static GOT_OFFER: AtomicBool = AtomicBool::new(false);
static LAST_OFFER: Mutex<Vec<u8>> = Mutex::new(Vec::new());
extern "C" fn post_message(
_user_data: *mut c_void,
_destination_id: *const std::ffi::c_char,
_destination_id_len: usize,
_message_namespace: *const std::ffi::c_char,
_message_namespace_len: usize,
message: *const std::ffi::c_char,
message_len: usize,
) {
let bytes = unsafe { std::slice::from_raw_parts(message as *const u8, message_len) };
*LAST_OFFER.lock().unwrap() = bytes.to_vec();
GOT_OFFER.store(true, Ordering::Release);
}
extern "C" fn on_negotiated(_user_data: *mut c_void) {}
extern "C" fn on_error(_user_data: *mut c_void, message: *const std::ffi::c_char, message_len: usize) {
let bytes = unsafe { std::slice::from_raw_parts(message as *const u8, message_len) };
panic!("session error: {}", String::from_utf8_lossy(bytes));
}
extern "C" fn on_picture_lost(_user_data: *mut c_void) {}
#[test]
fn create_negotiate_sends_offer_and_tears_down_cleanly() {
let remote_ip = "192.0.2.1";
let local_source_id = "sender-0";
let receiver_id = "receiver-0";
let sender = unsafe {
breadcast_caststream_sender_create(
remote_ip.as_ptr() as *const _,
remote_ip.len(),
local_source_id.as_ptr() as *const _,
local_source_id.len(),
receiver_id.as_ptr() as *const _,
receiver_id.len(),
1920,
1080,
8_000_000,
30,
1,
std::ptr::null_mut(),
post_message,
on_negotiated,
on_error,
on_picture_lost,
)
};
assert!(!sender.is_null(), "sender_create returned null");
unsafe { breadcast_caststream_sender_negotiate(sender) };
let deadline = Instant::now() + Duration::from_secs(2);
while !GOT_OFFER.load(Ordering::Acquire) && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
assert!(GOT_OFFER.load(Ordering::Acquire), "never received OFFER via post_message");
let offer = String::from_utf8(LAST_OFFER.lock().unwrap().clone()).unwrap();
assert!(offer.contains("\"type\":\"OFFER\""), "unexpected payload: {offer}");
assert!(offer.contains("\"codecName\":\"h264\""), "unexpected payload: {offer}");
// Before negotiation with a real receiver completes, these should still
// return sane defaults rather than garbage/crashing.
let needs_key = unsafe { breadcast_caststream_sender_needs_key_frame(sender) };
let bandwidth = unsafe { breadcast_caststream_sender_estimated_bandwidth_bps(sender) };
assert_eq!(needs_key, 1);
assert!(bandwidth > 0);
unsafe { breadcast_caststream_sender_destroy(sender) };
}