use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use orfail::OrFail;
#[derive(Debug, Clone)]
pub struct RecordingMetadata {
pub split_only: bool,
pub archives: Vec<ArchiveEntry>,
}
impl<'text, 'raw> TryFrom<nojson::RawJsonValue<'text, 'raw>> for RecordingMetadata {
type Error = nojson::JsonParseError;
fn try_from(value: nojson::RawJsonValue<'text, 'raw>) -> Result<Self, Self::Error> {
let archives = value.to_member("archives")?.required()?;
let split_only = value.to_member("split_only")?;
Ok(Self {
split_only: Option::unwrap_or_default(split_only.try_into()?),
archives: archives.try_into()?,
})
}
}
impl RecordingMetadata {
pub fn from_file<P: AsRef<Path>>(path: P) -> orfail::Result<Self> {
crate::json::parse_file(path).or_fail()
}
pub fn archive_metadata_paths(&self) -> orfail::Result<Vec<PathBuf>> {
if self.split_only {
let mut paths = Vec::new();
for archive in &self.archives {
let last_index_str = archive.split_last_index.as_ref().or_fail()?;
let last_index = last_index_str.parse::<usize>().or_fail()?;
for i in 1..=last_index {
let path = format!("split-archive-{}_{:04}.json", archive.connection_id, i);
paths.push(PathBuf::from(path));
}
}
Ok(paths)
} else {
Ok(self
.archives
.iter()
.filter_map(|a| a.metadata_filename.clone())
.collect())
}
}
}
#[derive(Debug, Clone)]
pub struct ArchiveEntry {
pub connection_id: String,
pub split_last_index: Option<String>,
pub metadata_filename: Option<PathBuf>,
}
impl<'text, 'raw> TryFrom<nojson::RawJsonValue<'text, 'raw>> for ArchiveEntry {
type Error = nojson::JsonParseError;
fn try_from(value: nojson::RawJsonValue<'text, 'raw>) -> Result<Self, Self::Error> {
let connection_id = value.to_member("connection_id")?.required()?;
let split_last_index = value.to_member("split_last_index")?;
let metadata_filename = value.to_member("metadata_filename")?;
Ok(Self {
connection_id: connection_id.try_into()?,
split_last_index: Option::unwrap_or_default(split_last_index.try_into()?),
metadata_filename: Option::unwrap_or_default(metadata_filename.try_into()?),
})
}
}
#[derive(Debug, Clone)]
pub struct ArchiveMetadata {
pub connection_id: String,
pub format: ContainerFormat,
pub audio: bool,
pub video: bool,
pub start_time_offset: u64,
pub stop_time_offset: u64,
}
impl<'text, 'raw> TryFrom<nojson::RawJsonValue<'text, 'raw>> for ArchiveMetadata {
type Error = nojson::JsonParseError;
fn try_from(value: nojson::RawJsonValue<'text, 'raw>) -> Result<Self, Self::Error> {
let connection_id = value.to_member("connection_id")?.required()?;
let format = value
.to_member("format")?
.map(ContainerFormat::try_from)?
.unwrap_or(ContainerFormat::Webm); let audio = value.to_member("audio")?.required()?;
let video = value.to_member("video")?.required()?;
let start_time_offset = value.to_member("start_time_offset")?.required()?;
let stop_time_offset = value.to_member("stop_time_offset")?.required()?;
Ok(Self {
connection_id: connection_id.try_into()?,
format,
start_time_offset: start_time_offset.try_into()?,
stop_time_offset: stop_time_offset.try_into()?,
audio: audio.as_raw_str() != "false",
video: video.as_raw_str() != "false",
})
}
}
impl ArchiveMetadata {
pub fn from_file<P: AsRef<Path>>(path: P) -> orfail::Result<Self> {
crate::json::parse_file(path).or_fail()
}
pub fn source_id(&self) -> SourceId {
SourceId(Arc::new(self.connection_id.clone()))
}
pub fn source_info(&self) -> SourceInfo {
SourceInfo {
id: self.source_id(),
format: self.format,
start_timestamp: Duration::from_secs(self.start_time_offset),
stop_timestamp: Duration::from_secs(self.stop_time_offset),
audio: self.audio,
video: self.video,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SourceId(Arc<String>);
impl nojson::DisplayJson for SourceId {
fn fmt(&self, f: &mut nojson::JsonFormatter<'_, '_>) -> std::fmt::Result {
f.value(&*self.0)
}
}
impl SourceId {
pub fn new(id: &str) -> Self {
Self(Arc::new(id.to_owned()))
}
pub fn get(&self) -> &str {
&self.0
}
}
impl From<SourceId> for String {
fn from(value: SourceId) -> Self {
(*value.0).clone()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceInfo {
pub id: SourceId,
pub format: ContainerFormat,
pub audio: bool,
pub video: bool,
pub start_timestamp: Duration,
pub stop_timestamp: Duration,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ContainerFormat {
#[default]
Webm,
Mp4,
}
impl ContainerFormat {
pub fn from_path<P: AsRef<Path>>(path: P) -> orfail::Result<Self> {
let ext = path
.as_ref()
.extension()
.or_fail_with(|()| format!("no media file extension: {}", path.as_ref().display()))?;
if ext == "mp4" {
Ok(Self::Mp4)
} else if ext == "webm" {
Ok(Self::Webm)
} else {
Err(orfail::Failure::new(format!(
"unexpected media file extension: {}",
path.as_ref().display()
)))
}
}
}
impl nojson::DisplayJson for ContainerFormat {
fn fmt(&self, f: &mut nojson::JsonFormatter<'_, '_>) -> std::fmt::Result {
match self {
ContainerFormat::Webm => f.string("webm"),
ContainerFormat::Mp4 => f.string("mp4"),
}
}
}
impl<'text, 'raw> TryFrom<nojson::RawJsonValue<'text, 'raw>> for ContainerFormat {
type Error = nojson::JsonParseError;
fn try_from(value: nojson::RawJsonValue<'text, 'raw>) -> Result<Self, Self::Error> {
match value.to_unquoted_string_str()?.as_ref() {
"webm" => Ok(Self::Webm),
"mp4" => Ok(Self::Mp4),
v => Err(value.invalid(format!("unknown container format: {v}"))),
}
}
}