Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/core/src/bc/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ pub const MSG_ID_GET_SERVICE_PORTS: u32 = 37;
pub const MSG_ID_GET_EMAIL: u32 = 42;
/// Set email settings
pub const MSG_ID_SET_EMAIL: u32 = 43;
/// Get compression config
pub const MSG_ID_GET_COMPRESSION: u32 = 56;
/// Set compression config
pub const MSG_ID_SET_COMPRESSION: u32 = 57;
/// Get users and general system info
pub const MSG_ID_GET_ABILITY_SUPPORT: u32 = 58;
/// Update, create and remove users
Expand Down
61 changes: 61 additions & 0 deletions crates/core/src/bc/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ pub struct BcXml {
/// Read and write users
#[serde(rename = "UserList", skip_serializing_if = "Option::is_none")]
pub user_list: Option<UserList>,
/// Compression/encoding settings
#[serde(rename = "Compression", skip_serializing_if = "Option::is_none")]
pub compression: Option<Compression>,
}

impl BcXml {
Expand Down Expand Up @@ -423,6 +426,64 @@ pub struct LedState {
pub light_state: String,
}

/// GOP (group of pictures) settings
#[allow(missing_docs)]
#[derive(PartialEq, Eq, Default, Debug, Deserialize, Serialize, Clone)]
pub struct GopSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub cur: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<u32>,
}

/// Per-stream compression/encoding settings
#[allow(missing_docs)]
#[derive(PartialEq, Eq, Default, Debug, Deserialize, Serialize, Clone)]
pub struct StreamCompression {
#[serde(skip_serializing_if = "Option::is_none")]
pub audio: Option<u32>,
#[serde(rename = "resolutionName", skip_serializing_if = "Option::is_none")]
pub resolution_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
/// Rate control mode (e.g. "cbr", "vbr")
#[serde(rename = "encoderType", skip_serializing_if = "Option::is_none")]
pub encoder_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frame: Option<u32>,
/// Bitrate in kbps
#[serde(rename = "bitRate", skip_serializing_if = "Option::is_none")]
pub bit_rate: Option<u32>,
#[serde(rename = "encoderProfile", skip_serializing_if = "Option::is_none")]
pub encoder_profile: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gop: Option<GopSettings>,
#[serde(rename = "videoEncType", skip_serializing_if = "Option::is_none")]
pub video_enc_type: Option<String>,
}

/// Compression/encoding settings (MSG 56/57)
#[allow(missing_docs)]
#[derive(PartialEq, Eq, Default, Debug, Deserialize, Serialize, Clone)]
pub struct Compression {
#[serde(rename = "@version")]
pub version: String,
#[serde(rename = "channelId")]
pub channel_id: u8,
#[serde(rename = "isNoTranslateFrame", skip_serializing_if = "Option::is_none")]
pub is_no_translate_frame: Option<u32>,
#[serde(rename = "mainStream", skip_serializing_if = "Option::is_none")]
pub main_stream: Option<StreamCompression>,
#[serde(rename = "subStream", skip_serializing_if = "Option::is_none")]
pub sub_stream: Option<StreamCompression>,
#[serde(rename = "thirdStream", skip_serializing_if = "Option::is_none")]
pub third_stream: Option<StreamCompression>,
}

/// FloodlightStatus xml
#[derive(PartialEq, Eq, Default, Debug, Deserialize, Serialize, Clone)]
pub struct FloodlightStatus {
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/bc_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use Md5Trunc::*;

mod abilityinfo;
mod battery;
mod compression;
mod connection;
mod credentials;
mod email;
Expand Down
107 changes: 107 additions & 0 deletions crates/core/src/bc_protocol/compression.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use super::{BcCamera, Error, Result};
use crate::bc::{model::*, xml::*};

impl BcCamera {
/// Get the current compression/encoding settings
pub async fn get_compression(&self) -> Result<Compression> {
self.has_ability_ro("compress").await?;
let connection = self.get_connection();
let msg_num = self.new_message_num();
let mut sub_get = connection.subscribe(MSG_ID_GET_COMPRESSION, msg_num).await?;
let get = Bc {
meta: BcMeta {
msg_id: MSG_ID_GET_COMPRESSION,
channel_id: self.channel_id,
msg_num,
response_code: 0,
stream_type: 0,
class: 0x6414,
},
body: BcBody::ModernMsg(ModernMsg {
extension: Some(Extension {
channel_id: Some(self.channel_id),
..Default::default()
}),
payload: None,
}),
};

sub_get.send(get).await?;
let msg = sub_get.recv().await?;
if msg.meta.response_code != 200 {
return Err(Error::CameraServiceUnavailable {
id: msg.meta.msg_id,
code: msg.meta.response_code,
});
}

if let BcBody::ModernMsg(ModernMsg {
payload:
Some(BcPayloads::BcXml(BcXml {
compression: Some(compression),
..
})),
..
}) = msg.body
{
Ok(compression)
} else {
Err(Error::UnintelligibleReply {
reply: std::sync::Arc::new(Box::new(msg)),
why: "Expected Compression xml but it was not received",
})
}
}

/// Set compression/encoding settings
pub async fn set_compression(&self, compression: Compression) -> Result<()> {
self.has_ability_rw("compress").await?;
let connection = self.get_connection();

let msg_num = self.new_message_num();
let mut sub_set = connection.subscribe(MSG_ID_SET_COMPRESSION, msg_num).await?;

let set = Bc {
meta: BcMeta {
msg_id: MSG_ID_SET_COMPRESSION,
channel_id: self.channel_id,
msg_num,
response_code: 0,
stream_type: 0,
class: 0x6414,
},
body: BcBody::ModernMsg(ModernMsg {
extension: Some(Extension {
channel_id: Some(self.channel_id),
..Default::default()
}),
payload: Some(BcPayloads::BcXml(BcXml {
compression: Some(compression),
..Default::default()
})),
}),
};

sub_set.send(set).await?;
if let Ok(reply) =
tokio::time::timeout(tokio::time::Duration::from_millis(500), sub_set.recv()).await
{
let msg = reply?;

if let BcMeta {
response_code: 200, ..
} = msg.meta
{
Ok(())
} else {
Err(Error::UnintelligibleReply {
reply: std::sync::Arc::new(Box::new(msg)),
why: "The camera did not accept the Compression xml",
})
}
} else {
// Some cameras seem to just not send a reply on success
Ok(())
}
}
}
1 change: 1 addition & 0 deletions src/cmdline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ pub enum Command {
Battery(super::battery::Opt),
Services(super::services::Opt),
Users(super::users::Opt),
Encoding(super::encoding::Opt),
}
75 changes: 75 additions & 0 deletions src/encoding/cmdline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use clap::{Parser, Subcommand, ValueEnum};

/// Control video encoding settings per stream
#[derive(Parser, Debug)]
pub struct Opt {
/// The name of the camera. Must be a name in the config
pub camera: String,
#[command(subcommand)]
pub cmd: EncodingAction,
}

#[derive(Subcommand, Debug)]
pub enum EncodingAction {
/// Get the current encoding settings for all streams
Get,
/// Set encoding parameters for a stream
Set {
/// Which stream to modify
#[arg(long)]
stream: StreamName,
/// Bitrate in kbps (e.g. 4096)
#[arg(long)]
bitrate: Option<u32>,
/// Frames per second (e.g. 15)
#[arg(long)]
fps: Option<u32>,
/// Rate control mode
#[arg(long)]
rate_control: Option<RateControl>,
/// Encoder profile
#[arg(long)]
profile: Option<Profile>,
},
}

#[derive(Debug, Clone, ValueEnum)]
pub enum StreamName {
Main,
Sub,
Third,
}

#[derive(Debug, Clone, ValueEnum)]
pub enum RateControl {
Cbr,
Vbr,
}

#[derive(Debug, Clone, ValueEnum)]
pub enum Profile {
Default,
BaseLine,
High,
Main,
}

impl std::fmt::Display for RateControl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RateControl::Cbr => write!(f, "cbr"),
RateControl::Vbr => write!(f, "vbr"),
}
}
}

impl std::fmt::Display for Profile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Profile::Default => write!(f, "default"),
Profile::BaseLine => write!(f, "baseLine"),
Profile::High => write!(f, "high"),
Profile::Main => write!(f, "main"),
}
}
}
Loading