use std::borrow::Cow;
use std::path::Path;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use busybar_render::{ImageFormat, Raster};
use busylib::TungsteniteWsTransport;
use busylib::proto::bsb_frame::{Frame as StreamedFrame, Screen};
use busylib::proto::bsb_state::State;
use busylib::proto::bsb_state::state_update::State as Update;
use crate::cli::Context;
use crate::error::Result;
use crate::io::Io;
use crate::reporter::{FramePayload, StreamingStatusEvent};
use crate::types::frame::{Frame, screen_name};
use crate::types::image_format_arg::ImageFormatArg;
use crate::types::output_format::OutputFormatArg;
use crate::types::screen_arg::ScreenArg;
pub async fn run(
context: &Context,
directory: &Path,
screen: ScreenArg,
no_image_raster: bool,
) -> Result<()> {
let screen = Screen::from(screen);
let raster = (!no_image_raster && screen == Screen::Front).then(Raster::default);
Io::create_dir(directory)?;
let transport = TungsteniteWsTransport::new();
let mut stream = context.client.streaming().status_ws(&transport).await?;
let mut sequence = 0u64;
loop {
tokio::select! {
_ = tokio::signal::ctrl_c() => break,
message = stream.next() => match message {
Some(message) => {
for event in events(context, &mut sequence, message?, directory, screen, raster) {
context.reporter.report(event)?;
}
}
None => break,
},
}
}
stream.close().await?;
Ok(())
}
fn events(
context: &Context,
sequence: &mut u64,
state: State,
directory: &Path,
screen: Screen,
raster: Option<Raster>,
) -> Vec<StreamingStatusEvent> {
let error = StreamingStatusEvent::error_of(&state);
let timestamp = state.timestamp;
let mut events = Vec::new();
for update in state.updates {
let Some(Update::Frame(frame)) = update.state else {
continue;
};
if frame.screen() != screen {
continue;
}
*sequence += 1;
let payload = capture(context, *sequence, &frame, directory, raster);
events.push(StreamingStatusEvent::frame(
*sequence, timestamp, error, payload,
));
}
events
}
fn capture(
context: &Context,
sequence: u64,
frame: &StreamedFrame,
directory: &Path,
raster: Option<Raster>,
) -> FramePayload {
let selected = context.image_format;
let re_encode = selected.image_format();
let inline = matches!(context.output_format, OutputFormatArg::Json);
let mut reason = None;
let decoded = match re_encode {
Some(_) => match Frame::from_streamed(frame).and_then(|image| match raster {
Some(raster) => image.with_raster(raster),
None => Ok(image),
}) {
Ok(image) => Some(image),
Err(error) => {
reason = Some(error.to_string());
None
}
},
None => None,
};
let path = if re_encode.is_none() || decoded.is_some() {
let name = format!("{}-{sequence:06}.{selected}", screen_name(frame.screen()));
match write_frame(&directory.join(name), frame, decoded.as_ref(), re_encode) {
Ok(path) => Some(path),
Err(error) => {
reason.get_or_insert(error.to_string());
None
}
}
} else {
None
};
let encoded = match (&decoded, re_encode) {
(Some(image), Some(format)) if inline => match image.encode_base64(format) {
Ok(encoded) => Some(encoded),
Err(error) => {
reason.get_or_insert(error.to_string());
None
}
},
_ => None,
};
let image = inline.then(|| match encoded {
Some(encoded) => (selected.to_string(), encoded),
None => (
ImageFormatArg::Raw.to_string(),
STANDARD.encode(&frame.data),
),
});
FramePayload::new(frame, image, path, reason)
}
fn write_frame(
path: &Path,
frame: &StreamedFrame,
decoded: Option<&Frame>,
format: Option<ImageFormat>,
) -> Result<String> {
let bytes = match (decoded, format) {
(Some(image), Some(format)) => Cow::Owned(image.encode(format)?),
_ => Cow::Borrowed(frame.data.as_slice()),
};
Io::write_bytes(path, &bytes)?;
Ok(path.display().to_string())
}