viva_camctl/
cmd_stream.rs1use std::net::Ipv4Addr;
2use std::path::PathBuf;
3use std::time::Duration;
4
5use anyhow::{Context, Result, anyhow, bail};
6use tokio::time::{self, Instant, MissedTickBehavior};
7use tracing::{info, warn};
8
9use viva_genicam::pfnc::PixelFormat;
10use viva_genicam::{Frame, FrameStream, StreamBuilder, StreamDest};
11
12use viva_gige::nic::IfaceSelector;
13
14use crate::common::{self, DEFAULT_DISCOVERY_TIMEOUT_MS};
15
16#[derive(Debug, Clone)]
17pub struct StreamArgs {
18 pub ip: Option<Ipv4Addr>,
19 pub index: Option<usize>,
20 pub iface: Option<IfaceSelector>,
21 pub mode: String,
22 pub group: Option<Ipv4Addr>,
23 pub port: u16,
24 pub packet_size: Option<u32>,
26 pub auto: bool,
29 pub save: usize,
30 pub rgb: bool,
31 pub duration_s: u64,
32}
33
34#[allow(clippy::await_holding_lock)]
40pub async fn run(args: StreamArgs) -> Result<()> {
41 let timeout = Duration::from_millis(DEFAULT_DISCOVERY_TIMEOUT_MS);
42 let device = common::select_device(args.ip, args.index, args.iface.as_ref(), timeout).await?;
43 info!(ip = %device.ip, "opening camera for streaming");
44 let mut camera = common::open_camera(&device)
45 .await
46 .context("open camera for stream")?;
47
48 let iface = common::resolve_receive_iface(args.iface.as_ref(), device.ip)?;
49 let host_ip = iface
50 .ipv4()
51 .ok_or_else(|| anyhow!("interface {} has no IPv4 address", iface.name()))?;
52 let mode = parse_mode(&args.mode)?;
53
54 if let StreamMode::Multicast = mode {
55 let group = args
56 .group
57 .ok_or_else(|| anyhow!("multicast mode requires --group"))?;
58 camera
59 .configure_stream_multicast(0, group, args.port)
60 .context("configure multicast destination")?;
61 }
62
63 let dest = match mode {
64 StreamMode::Unicast => StreamDest::Unicast {
65 dst_ip: host_ip,
66 dst_port: args.port,
67 },
68 StreamMode::Multicast => {
69 let group = args
70 .group
71 .ok_or_else(|| anyhow!("multicast mode requires --group"))?;
72 StreamDest::Multicast {
73 group,
74 port: args.port,
75 loopback: false,
76 ttl: 1,
77 }
78 }
79 };
80 let stream = {
90 let mut stream_device = camera
91 .transport()
92 .lock_device()
93 .context("access camera control channel for stream configuration")?;
94 stream_device
95 .claim_control()
96 .await
97 .context("claim camera control for stream configuration")?;
98
99 let mut builder = StreamBuilder::new(&mut stream_device)
100 .iface(iface.clone())
101 .dest(dest)
102 .rcvbuf_bytes(64 << 20);
103 if args.auto {
104 builder = builder.auto_packet_size();
105 } else if let Some(size) = args.packet_size {
106 builder = builder.packet_size(size);
107 }
108 if args.port != 0 {
109 builder = builder.destination_port(args.port);
110 }
111 builder.build().await.context("negotiate stream")?
112 };
113
114 let mut frame_stream = FrameStream::new(stream, None);
118 let stats = frame_stream.stats_handle();
121
122 if let Err(err) = camera.set("TLParamsLocked", "1") {
123 warn!(error = %err, "failed to lock transport-layer parameters");
124 }
125 camera.acquisition_start().context("start acquisition")?;
126 let mut saved_frames = 0usize;
127 let mut frame_index = 0usize;
128 let end_deadline = if args.duration_s > 0 {
129 Some(Instant::now() + Duration::from_secs(args.duration_s))
130 } else {
131 None
132 };
133 let mut interrupted = false;
134 let mut ctrl_c = Box::pin(tokio::signal::ctrl_c());
135 let mut ticker = time::interval(Duration::from_secs(1));
136 ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
137
138 loop {
139 if let Some(deadline) = end_deadline
140 && Instant::now() >= deadline
141 {
142 info!("stream duration elapsed");
143 break;
144 }
145
146 tokio::select! {
147 _ = ticker.tick() => {
148 let snapshot = stats.snapshot();
152 println!(
153 "[stream] fps={:.1} Mbps={:.2} frames={} drops={} resends={}",
154 snapshot.avg_fps,
155 snapshot.avg_mbps,
156 snapshot.frames,
157 snapshot.drops,
158 snapshot.resends,
159 );
160 }
161 _ = &mut ctrl_c => {
162 info!("received ctrl-c; stopping stream");
163 interrupted = true;
164 break;
165 }
166 received = frame_stream.next_frame() => {
167 match received {
168 Ok(Some(mut frame)) => {
169 if let Some(timestamp) = frame.ts_dev {
173 frame.ts_host = Some(camera.map_dev_ts(timestamp));
174 }
175 frame_index += 1;
176
177 if saved_frames < args.save {
178 if let Err(err) = save_frame(&frame, frame_index, args.rgb) {
179 warn!(error = %err, frame = frame_index, "failed to save frame");
180 } else {
181 saved_frames += 1;
182 }
183 }
184 }
185 Err(err) => {
186 warn!(error = %err, "stream receiver failed");
187 break;
188 }
189 Ok(None) => break,
190 }
191 }
192 }
193 }
194
195 camera.acquisition_stop().context("stop acquisition")?;
196 if let Err(err) = camera.set("TLParamsLocked", "0") {
197 warn!(error = %err, "failed to unlock transport-layer parameters");
198 }
199 if interrupted {
200 println!("Stream interrupted by user.");
201 }
202 let summary = stats.snapshot();
203 println!(
204 "Summary: frames={} bytes={} drops={} resends={} avg_fps={:.1} avg_mbps={:.2}",
205 summary.frames,
206 summary.bytes,
207 summary.drops,
208 summary.resends,
209 summary.avg_fps,
210 summary.avg_mbps,
211 );
212
213 Ok(())
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217enum StreamMode {
218 Unicast,
219 Multicast,
220}
221
222fn parse_mode(value: &str) -> Result<StreamMode> {
223 match value.to_ascii_lowercase().as_str() {
224 "unicast" => Ok(StreamMode::Unicast),
225 "multicast" => Ok(StreamMode::Multicast),
226 other => bail!("unknown stream mode '{other}' (expected unicast or multicast)"),
227 }
228}
229
230fn save_frame(frame: &Frame, index: usize, rgb: bool) -> Result<PathBuf> {
231 let (buffer, ext) = if !rgb && frame.pixel_format == PixelFormat::Mono8 {
232 let data = frame.payload.clone();
233 let encoded = common::encode_pgm(frame.width, frame.height, data.as_ref())?;
234 (encoded, "pgm")
235 } else {
236 let rgb_pixels = frame.to_rgb8().context("convert frame to RGB8")?;
237 let encoded = common::encode_ppm(frame.width, frame.height, &rgb_pixels)?;
238 (encoded, "ppm")
239 };
240 let path = PathBuf::from(format!("frame_{index:04}.{ext}"));
241 common::save_image(&buffer, &path)?;
242 info!(file = %path.display(), "saved frame");
243 Ok(path)
244}