1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
|
//! Stdout video backend for piping video output to other tools
use crate::error::{Result, VideoError};
use crate::video::{VideoBackendTrait, VideoStats};
use std::io::{self, Write};
use std::sync::Mutex;
use tracing::{debug, info};
/// Configuration for the stdout video backend
#[derive(Debug, Clone)]
pub struct StdoutConfig {
/// Whether to output frame headers with metadata
pub include_headers: bool,
/// Whether to flush stdout after each frame
pub flush_after_frame: bool,
/// Output format for headers (if enabled)
pub header_format: HeaderFormat,
}
impl Default for StdoutConfig {
fn default() -> Self {
Self {
include_headers: false,
flush_after_frame: true,
header_format: HeaderFormat::Simple,
}
}
}
/// Header format for frame metadata
#[derive(Debug, Clone)]
pub enum HeaderFormat {
/// Simple text format: "FRAME:size:timestamp\n"
Simple,
/// JSON format: {"frame": {"size": size, "timestamp": timestamp}}
Json,
/// Binary format: 4-byte size + 8-byte timestamp
Binary,
}
/// Stdout video backend that outputs raw video frames to stdout
pub struct StdoutBackend {
config: StdoutConfig,
stats: Mutex<VideoStats>,
frame_count: Mutex<u64>,
start_time: Mutex<std::time::Instant>,
}
impl StdoutBackend {
/// Create a new stdout backend with default configuration
pub fn new() -> Self {
Self {
config: StdoutConfig::default(),
stats: Mutex::new(VideoStats::default()),
frame_count: Mutex::new(0),
start_time: Mutex::new(std::time::Instant::now()),
}
}
/// Create a new stdout backend with custom configuration
pub fn with_config(config: StdoutConfig) -> Self {
Self {
config,
stats: Mutex::new(VideoStats::default()),
frame_count: Mutex::new(0),
start_time: Mutex::new(std::time::Instant::now()),
}
}
/// Write frame header if enabled
fn write_frame_header(&self, frame_size: usize) -> Result<()> {
if !self.config.include_headers {
return Ok(());
}
let start_time = self.start_time.lock().unwrap();
let timestamp = start_time.elapsed().as_micros();
drop(start_time);
match self.config.header_format {
HeaderFormat::Simple => {
let header = format!("FRAME:{}:{}\n", frame_size, timestamp);
io::stdout().write_all(header.as_bytes())?;
}
HeaderFormat::Json => {
let header = format!(
r#"{{"frame": {{"size": {}, "timestamp": {}}}}}{}"#,
frame_size, timestamp, '\n'
);
io::stdout().write_all(header.as_bytes())?;
}
HeaderFormat::Binary => {
// 4-byte size + 8-byte timestamp
let size_bytes = (frame_size as u32).to_le_bytes();
let timestamp_bytes = timestamp.to_le_bytes();
let mut header = Vec::with_capacity(12);
header.extend_from_slice(&size_bytes);
header.extend_from_slice(×tamp_bytes);
io::stdout().write_all(&header)?;
}
}
Ok(())
}
/// Update statistics
fn update_stats(&self, frame_size: usize) {
let mut frame_count = self.frame_count.lock().unwrap();
*frame_count += 1;
let current_frame_count = *frame_count;
drop(frame_count);
let mut stats = self.stats.lock().unwrap();
stats.frames_pushed = current_frame_count;
stats.total_bytes += frame_size as u64;
// Calculate FPS based on elapsed time
let start_time = self.start_time.lock().unwrap();
let elapsed = start_time.elapsed().as_secs_f64();
drop(start_time);
if elapsed > 0.0 {
stats.fps = current_frame_count as f64 / elapsed;
}
// Log frame information for debugging
if current_frame_count % 30 == 0 { // Log every 30 frames
tracing::debug!(
"Stdout backend: frame {}, size: {} bytes, total: {} bytes, fps: {:.2}",
current_frame_count, frame_size, stats.total_bytes, stats.fps
);
}
}
}
impl VideoBackendTrait for StdoutBackend {
fn initialize(&mut self) -> Result<()> {
info!("Initializing stdout video backend");
{
let mut start_time = self.start_time.lock().unwrap();
*start_time = std::time::Instant::now();
}
{
let mut stats = self.stats.lock().unwrap();
stats.is_ready = true;
}
// Write initial header if enabled
if self.config.include_headers {
match self.config.header_format {
HeaderFormat::Simple => {
io::stdout().write_all(b"STDOUT_VIDEO_STREAM_START\n")?;
}
HeaderFormat::Json => {
io::stdout().write_all(b"{\"stream\": \"start\"}\n")?;
}
HeaderFormat::Binary => {
// Magic number: "STDO" (4 bytes)
io::stdout().write_all(b"STDO")?;
}
}
}
debug!("Stdout video backend initialized successfully");
Ok(())
}
fn push_frame(&self, frame_data: &[u8]) -> Result<()> {
let stats = self.stats.lock().unwrap();
if !stats.is_ready {
return Err(VideoError::DeviceNotReady.into());
}
drop(stats);
// Write frame header if enabled
self.write_frame_header(frame_data.len())?;
// Write frame data to stdout
io::stdout().write_all(frame_data)?;
// Flush if configured
if self.config.flush_after_frame {
io::stdout().flush()?;
}
// Update statistics
self.update_stats(frame_data.len());
debug!("Pushed frame to stdout: {} bytes", frame_data.len());
Ok(())
}
fn get_stats(&self) -> VideoStats {
let mut stats = self.stats.lock().unwrap().clone();
stats.backend_type = crate::video::VideoBackendType::Stdout;
stats
}
fn is_ready(&self) -> bool {
self.stats.lock().unwrap().is_ready
}
fn shutdown(&mut self) -> Result<()> {
info!("Shutting down stdout video backend");
// Write final header if enabled
if self.config.include_headers {
match self.config.header_format {
HeaderFormat::Simple => {
io::stdout().write_all(b"STDOUT_VIDEO_STREAM_END\n")?;
}
HeaderFormat::Json => {
io::stdout().write_all(b"{\"stream\": \"end\"}\n")?;
}
HeaderFormat::Binary => {
// End marker: "END\0" (4 bytes)
io::stdout().write_all(b"END\0")?;
}
}
}
// Final flush
io::stdout().flush()?;
{
let mut stats = self.stats.lock().unwrap();
stats.is_ready = false;
}
Ok(())
}
}
impl Drop for StdoutBackend {
fn drop(&mut self) {
let is_ready = {
let stats = self.stats.lock().unwrap();
stats.is_ready
};
if is_ready {
let _ = self.shutdown();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::video::VideoBackendTrait;
#[test]
fn test_stdout_backend_creation() {
let backend = StdoutBackend::new();
assert!(!backend.is_ready());
assert_eq!(backend.get_stats().frames_pushed, 0);
}
#[test]
fn test_stdout_backend_with_config() {
let config = StdoutConfig {
include_headers: true,
flush_after_frame: false,
header_format: HeaderFormat::Json,
};
let backend = StdoutBackend::with_config(config);
assert!(!backend.is_ready());
}
#[test]
fn test_header_format_creation() {
let simple = HeaderFormat::Simple;
let json = HeaderFormat::Json;
let binary = HeaderFormat::Binary;
// Just test that they can be created
assert!(matches!(simple, HeaderFormat::Simple));
assert!(matches!(json, HeaderFormat::Json));
assert!(matches!(binary, HeaderFormat::Binary));
}
}
|