Implement Cast Streaming mirroring, DLNA casting, daemon+GUI, and breadd integration
Some checks failed
dev release / build (push) Failing after 12s
Some checks failed
dev release / build (push) Failing after 12s
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.
This commit is contained in:
parent
887c29002f
commit
8c745d18e0
283 changed files with 36788 additions and 0 deletions
91
breadcast-caststream-sys/tests/smoke.rs
Normal file
91
breadcast-caststream-sys/tests/smoke.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
//! 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) };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue