Browse Source

use log, add configuration

master
eta 3 months ago
parent
commit
a6c039ff00
  1. 1
      .gitignore
  2. 112
      Cargo.lock
  3. 4
      Cargo.toml
  4. 46
      src/config.rs
  5. 85
      src/main.rs

1
.gitignore vendored

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
/target
*.png
*.h264
config.toml

112
Cargo.lock generated

@ -8,12 +8,32 @@ version = "1.0.2" @@ -8,12 +8,32 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.1.0"
@ -61,9 +81,13 @@ dependencies = [ @@ -61,9 +81,13 @@ dependencies = [
"byteorder",
"futures-util",
"image",
"log",
"openh264",
"pretty_env_logger",
"retina",
"serde",
"tokio",
"toml",
]
[[package]]
@ -222,6 +246,19 @@ version = "1.8.0" @@ -222,6 +246,19 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "env_logger"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
dependencies = [
"atty",
"humantime",
"log",
"regex",
"termcolor",
]
[[package]]
name = "exr"
version = "1.5.2"
@ -461,6 +498,15 @@ dependencies = [ @@ -461,6 +498,15 @@ dependencies = [
"sha2",
]
[[package]]
name = "humantime"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
dependencies = [
"quick-error",
]
[[package]]
name = "idna"
version = "0.3.0"
@ -546,6 +592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" @@ -546,6 +592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
"serde",
]
[[package]]
@ -790,6 +837,16 @@ version = "0.3.0" @@ -790,6 +837,16 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5"
[[package]]
name = "pretty_env_logger"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d"
dependencies = [
"env_logger",
"log",
]
[[package]]
name = "proc-macro2"
version = "1.0.47"
@ -799,6 +856,12 @@ dependencies = [ @@ -799,6 +856,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quote"
version = "1.0.21"
@ -869,12 +932,29 @@ dependencies = [ @@ -869,12 +932,29 @@ dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]]
name = "regex-syntax"
version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "retina"
version = "0.4.3"
@ -962,6 +1042,20 @@ name = "serde" @@ -962,6 +1042,20 @@ name = "serde"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e326c9ec8042f1b5da33252c8a37e9ffbd2c9bef0155215b6e6c80c790e05f91"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a3df25b0713732468deadad63ab9da1f1fd75a48a15024b50363f128db627e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sha2"
@ -1028,6 +1122,15 @@ dependencies = [ @@ -1028,6 +1122,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.37"
@ -1139,6 +1242,15 @@ dependencies = [ @@ -1139,6 +1242,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "toml"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
dependencies = [
"serde",
]
[[package]]
name = "tracing"
version = "0.1.37"

4
Cargo.toml

@ -11,4 +11,8 @@ anyhow = "1.0" @@ -11,4 +11,8 @@ anyhow = "1.0"
futures-util = "0.3"
byteorder = "1.4"
image = "0.24"
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
log = { version = "0.4", features = ["serde"] }
pretty_env_logger = "0.4"

46
src/config.rs

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
//! Configuration for the program and its probes.
use serde::Deserialize;
/// A probe: a request to export the brightness of a given pixel as an entity
/// in Home Assistant.
#[derive(Deserialize, Debug)]
pub struct Probe {
/// The X coordinate of the pixel to sample.
pub(crate) x: usize,
/// The Y coordinate of the pixel to sample.
pub(crate) y: usize,
/// A unique ID for this probe.
pub(crate) unique_id: String,
/// A Home Assistant icon for this probe.
pub(crate) icon: String,
/// A Home Assistant device class for this probe.
pub(crate) device_class: String,
/// A Home Assistant friendly name for this probe.
pub(crate) friendly_name: String,
}
fn default_log_level() -> log::LevelFilter {
log::LevelFilter::Info
}
/// Configuration for the program.
#[derive(Deserialize, Debug)]
pub struct Configuration {
/// RTSP URI to subscribe to.
pub(crate) rtsp_uri: String,
/// URI where the Home Assistant API can be reached.
pub(crate) home_assistant_uri: String,
/// Bearer token for Home Assistant.
pub(crate) home_assistant_token: String,
/// Set of probes.
pub(crate) probes: Vec<Probe>,
/// The luminance threshold (Y value) to class as "on".
pub(crate) on_luma: u8,
/// Log level to use.
#[serde(default = "default_log_level")]
pub(crate) loglevel: log::LevelFilter,
/// Path to save the first decoded image to.
#[serde(default)]
pub(crate) image_path: Option<String>,
}

85
src/main.rs

@ -2,27 +2,77 @@ use anyhow::{anyhow, Context}; @@ -2,27 +2,77 @@ use anyhow::{anyhow, Context};
use byteorder::{BigEndian, ReadBytesExt};
use futures_util::StreamExt;
use image::{ImageBuffer, Rgb};
use log::{debug, error, info, trace, warn};
use openh264::decoder::{Decoder, DecoderConfig};
use retina::client::{PlayOptions, Session, SessionOptions, SetupOptions};
use retina::codec::{CodecItem, ParametersRef};
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::ops::Deref;
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom};
use std::time::{Duration, Instant};
mod config;
use crate::config::{Configuration, Probe};
fn print_usage() {
let our_name = std::env::args()
.next()
.unwrap_or_else(|| "boiler-stats".to_owned());
eprintln!("usage: {} [configuration file path]", our_name);
}
fn load_config(path: &str) -> anyhow::Result<Configuration> {
let mut file = File::open(path).context(format!(
"failed to open config file at {}; check -h for usage",
path
))?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)
.context("failed to read config")?;
let ret = toml::from_slice(&buffer).context("failed to deserialize config")?;
Ok(ret)
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
eprintln!("[+] setting up RTSP stream");
let mut config_path = "./config.toml".to_owned();
match std::env::args().nth(1).as_ref().map(|x| x as &str) {
Some("-h") => {
print_usage();
return Ok(());
}
Some(path) => {
config_path = path.to_owned(); // eww
}
None => {}
}
let config = load_config(&config_path)?;
pretty_env_logger::formatted_builder()
.filter_level(config.loglevel)
.init();
info!("boiler-stats v{}", env!("CARGO_PKG_VERSION"));
info!("connecting to RTSP stream at {}", config.rtsp_uri);
let uri = config.rtsp_uri.parse().context("parsing RTSP URI")?;
debug!("doing DESCRIBE");
let mut session = Session::describe(
"rtsp://hammersmith.i.eta.st:8554/boiler".parse()?,
uri,
SessionOptions::default().user_agent("boiler-stats/0.1".to_owned()),
)
.await
.context("rtsp describe failed")?;
let mut stream_idx = None;
let mut video_params = None;
for (i, stream) in session.streams().iter().enumerate() {
eprintln!(
"[+] stream {}: media {} with encoding {} (parameters {})",
debug!(
"stream {}: media {} with encoding {} (parameters {})",
i,
stream.media(),
stream.encoding_name(),
@ -49,13 +99,14 @@ async fn main() -> anyhow::Result<()> { @@ -49,13 +99,14 @@ async fn main() -> anyhow::Result<()> {
.await
.context("rtsp play failed")?;
let mut demuxed = playing.demuxed().context("rtsp demux failed")?;
eprintln!("[+] streaming");
debug!("rtsp succeeded; starting openh264");
let mut decoder =
Decoder::with_config(DecoderConfig::default()).context("init openh264 decoder")?;
let mut provided_headers = false;
let mut imaged = false;
let mut last_success = Instant::now();
info!("streaming frames");
while let Some(ret) = demuxed.next().await {
let ret = match ret {
Ok(v) => v,
@ -79,7 +130,7 @@ async fn main() -> anyhow::Result<()> { @@ -79,7 +130,7 @@ async fn main() -> anyhow::Result<()> {
let nal_type = buf[0] & 0x1f;
// 7 = SPS, 8 = PPS
if nal_type != 7 && nal_type != 8 && !provided_headers {
eprintln!("[+] detected no in-band headers, passing them manually");
warn!("detected no in-band SPS/PPS NALs, passing them manually");
provided_headers = true;
let video_params = video_params
.take()
@ -108,9 +159,7 @@ async fn main() -> anyhow::Result<()> { @@ -108,9 +159,7 @@ async fn main() -> anyhow::Result<()> {
extra_data_cursor
.read_exact(&mut pps_nal[3..])
.context("failed to read PPS NAL")?;
assert_eq!(sps_nal[3] & 0x1f, 7);
assert_eq!(pps_nal[3] & 0x1f, 8);
eprintln!(
debug!(
"[+] SPS NAL length {}, PPS NAL length {}",
sps_nal.len(),
pps_nal.len()
@ -122,7 +171,7 @@ async fn main() -> anyhow::Result<()> { @@ -122,7 +171,7 @@ async fn main() -> anyhow::Result<()> {
decoder
.decode(&pps_nal)
.context("failed to decode PPS NAL")?;
eprintln!("[+] passed SPS and PPS NALs, continuing");
debug!("passed SPS and PPS NALs, continuing");
}
let decode_result = decoder.decode(&buf);
@ -135,10 +184,14 @@ async fn main() -> anyhow::Result<()> { @@ -135,10 +184,14 @@ async fn main() -> anyhow::Result<()> {
let ibuf =
ImageBuffer::<Rgb<u8>, _>::from_vec(width as u32, height as u32, buf)
.unwrap();
eprintln!("[+] frame at T+{:.02}s", vf.timestamp().elapsed_secs());
trace!("frame at T+{:.02}s", vf.timestamp().elapsed_secs());
if !imaged {
ibuf.save("./decoded.png")?;
eprintln!("[+] saved image to ./decoded.png");
if let Some(ip) = config.image_path.as_ref() {
ibuf.save(ip)?;
info!("saved first decoded frame to {}", ip);
} else {
info!("got first decoded frame");
}
imaged = true;
}
}
@ -146,7 +199,9 @@ async fn main() -> anyhow::Result<()> { @@ -146,7 +199,9 @@ async fn main() -> anyhow::Result<()> {
last_success = Instant::now();
}
Err(e) => {
trace!("decode error: {}", e);
if Instant::now() - last_success > Duration::from_secs(10) {
error!("giving up on decoding");
return Err(anyhow!(
"failed to decode after 10 seconds; last error: {}",
e

Loading…
Cancel
Save