From a1f915a75b84c9bb15aad35365e929e61a6e93f1 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Sun, 24 May 2026 10:39:36 -0500 Subject: [PATCH 1/2] feat: add more data to dossier --- .../ersatztv-channel/src/channel_session.rs | 34 +++- crates/ersatztv-channel/src/dossier.rs | 188 +++++++++++++++--- crates/ffpipeline/src/accel/amf.rs | 4 +- crates/ffpipeline/src/accel/cuda.rs | 4 +- crates/ffpipeline/src/accel/qsv.rs | 4 +- crates/ffpipeline/src/accel/rkmpp.rs | 4 +- crates/ffpipeline/src/accel/vaapi.rs | 6 +- crates/ffpipeline/src/accel/video_toolbox.rs | 4 +- crates/ffpipeline/src/accel/vulkan.rs | 4 +- .../ffpipeline/src/capabilities/nvidia/mod.rs | 6 +- .../ffpipeline/src/capabilities/opencl/mod.rs | 4 +- crates/ffpipeline/src/capabilities/qsv/mod.rs | 5 +- .../ffpipeline/src/capabilities/rkmpp/mod.rs | 4 +- .../ffpipeline/src/capabilities/vaapi/mod.rs | 100 +++++++++- .../src/capabilities/videotoolbox/mod.rs | 4 +- .../ffpipeline/src/capabilities/vulkan/mod.rs | 4 +- crates/ffpipeline/src/ffmpeg_info.rs | 3 +- crates/ffpipeline/src/frame_rate.rs | 4 +- crates/ffpipeline/src/hw_accel.rs | 4 +- crates/ffpipeline/src/pipeline.rs | 3 +- crates/ffpipeline/src/probe.rs | 14 +- 21 files changed, 340 insertions(+), 67 deletions(-) diff --git a/crates/ersatztv-channel/src/channel_session.rs b/crates/ersatztv-channel/src/channel_session.rs index 1c32b48..c283642 100644 --- a/crates/ersatztv-channel/src/channel_session.rs +++ b/crates/ersatztv-channel/src/channel_session.rs @@ -29,7 +29,7 @@ use time::OffsetDateTime; use tokio::io::AsyncBufReadExt; use tokio::sync::Mutex; -use crate::dossier::Dossier; +use crate::dossier::DossierBuilder; use crate::local_proxy::{LocalProxyServer, ScriptCommand}; use crate::playlist_manager::{PlaylistManager, PlaylistManagerOutputFiles, SubtitleSource}; use crate::playout_loader::PlayoutLoader; @@ -548,7 +548,7 @@ impl ChannelSession { .and_then(|s| s.stream_index); let subtitle_input = match ( - subtitle_probe_result, + subtitle_probe_result.clone(), subtitle_input_source, subtitle_timing, ) { @@ -568,7 +568,7 @@ impl ChannelSession { input_source: audio_input_source, in_point: audio_timing.in_point, out_point: audio_timing.out_point, - probe_result: audio_probe_result, + probe_result: audio_probe_result.clone(), stream_index: audio_index, }, video_input: ProbedInput { @@ -579,7 +579,7 @@ impl ChannelSession { video_timing.in_point }, out_point: video_timing.out_point, - probe_result: video_probe_result, + probe_result: video_probe_result.clone(), stream_index: video_index, }, subtitle_input, @@ -661,14 +661,32 @@ impl ChannelSession { let status = status.map_err(|e| ChannelError::StreamFailure(e.to_string()))?; let _ = reader_handle.await; if !status.success() { - let stderr_tail = ring + let stderr_tail: Vec<_> = ring .lock() .map(|r| r.iter().cloned().collect()) .unwrap_or_default(); - let report_source_file = self.channel_config.ffmpeg.reports_folder.as_ref().map(|folder| { + + let mut builder = DossierBuilder::new(&self.channel_config, &self.ffmpeg_info) + .item(current_item) + .stderr(stderr_tail) + .video(&video_probe_result) + .audio(&audio_probe_result); + + if let Some(accel) = &self.hw_accel { + builder = builder.accel(accel); + } + + if let Some(subtitle_probe_result) = &subtitle_probe_result { + builder = builder.subtitle(subtitle_probe_result); + } + + if let Some(report_source_file) = self.channel_config.ffmpeg.reports_folder.as_ref().map(|folder| { PathBuf::from(folder).join(format!(".in-flight-{}.log", self.channel_config.number())) - }); - let dossier = Dossier::new(&self.channel_config, current_item, stderr_tail, report_source_file); + }) { + builder = builder.report_source(report_source_file); + } + + let dossier = builder.build(); if let Err(err) = dossier.write().await { log::error!("failed to save dossier: {err}"); } diff --git a/crates/ersatztv-channel/src/dossier.rs b/crates/ersatztv-channel/src/dossier.rs index 9229c9a..7a6a008 100644 --- a/crates/ersatztv-channel/src/dossier.rs +++ b/crates/ersatztv-channel/src/dossier.rs @@ -2,44 +2,57 @@ use std::path::PathBuf; use ersatztv_channel::config::ChannelConfig; use ersatztv_channel::error::ChannelError; -use ersatztv_playout::playout::PlayoutItem; +use ersatztv_playout::playout::{DATE_FORMAT, PlayoutItem}; +use ffpipeline::ffmpeg_info::FfmpegInfo; +use ffpipeline::hw_accel::HardwareAccel; +use ffpipeline::probe::ProbeResult; +use serde::Serialize; use time::OffsetDateTime; +#[derive(Serialize)] +struct MediaInfo { + video: serde_json::Value, + audio: serde_json::Value, + subtitle: serde_json::Value, +} + +#[derive(Serialize)] +struct Pipeline { + ffmpeg_info: FfmpegInfo, + hw_accel: Option, +} + pub struct Dossier { channel_config: ChannelConfig, - item_id: String, - item_json: String, - stderr_tail: Vec, + pipeline: Pipeline, + item_id: Option, + item_json: Option, + media_info: Option, + stderr_tail: Option>, report_source_file: Option, } impl Dossier { - pub fn new( - channel_config: &ChannelConfig, - item: &PlayoutItem, - stderr_tail: Vec, - report_source_file: Option, - ) -> Self { - Self { - channel_config: channel_config.clone(), - item_id: item.id.clone(), - item_json: serde_json::to_string_pretty(item).unwrap_or_else(|_| String::from("{}")), - stderr_tail, - report_source_file, - } - } - pub async fn write(&self) -> Result<(), ChannelError> { if let Some(reports_folder) = self.channel_config.ffmpeg.reports_folder.as_ref() { - let timestamp = OffsetDateTime::now_utc().unix_timestamp_nanos(); + let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); + let formatted_now = now.format(&DATE_FORMAT)?; let reports_folder = PathBuf::from(reports_folder); - let dossier_folder = reports_folder.join(format!( - "{}_{}_{}", - self.channel_config.number(), - timestamp, - self.item_id - )); + let dossier_folder = if let Some(item_id) = &self.item_id { + reports_folder.join(format!( + "{}_{}_{}", + self.channel_config.number(), + formatted_now, + item_id + )) + } else { + reports_folder.join(format!( + "{}_{}", + self.channel_config.number(), + formatted_now + )) + }; tokio::fs::create_dir_all(&dossier_folder).await?; @@ -56,13 +69,29 @@ impl Dossier { } } - if !report_saved { + if !report_saved + && let Some(stderr_tail) = self.stderr_tail.as_ref().filter(|t| !t.is_empty()) + { let ffmpeg_stderr_file = dossier_folder.join("ffmpeg_stderr.log"); - tokio::fs::write(&ffmpeg_stderr_file, self.stderr_tail.join("\n")).await?; + tokio::fs::write(&ffmpeg_stderr_file, stderr_tail.join("\n")).await?; } - let playout_item_file = dossier_folder.join("playout_item.json"); - tokio::fs::write(&playout_item_file, &self.item_json).await?; + let pipeline_json = + serde_json::to_string_pretty(&self.pipeline).unwrap_or_else(|_| String::from("{}")); + let pipeline_file = dossier_folder.join("pipeline.json"); + tokio::fs::write(&pipeline_file, pipeline_json).await?; + + if let Some(item_json) = &self.item_json { + let playout_item_file = dossier_folder.join("playout_item.json"); + tokio::fs::write(&playout_item_file, item_json).await?; + } + + if let Some(media_info) = &self.media_info { + let media_info_json = + serde_json::to_string_pretty(media_info).unwrap_or_else(|_| String::from("{}")); + let media_info_file = dossier_folder.join("media_info.json"); + tokio::fs::write(&media_info_file, media_info_json).await?; + } let channel_config_json = serde_json::to_string_pretty(&self.channel_config) .unwrap_or_else(|_| String::from("{}")); @@ -73,3 +102,102 @@ impl Dossier { Ok(()) } } + +pub struct DossierBuilder { + channel_config: ChannelConfig, + pipeline: Pipeline, + item_id: Option, + item_json: Option, + media_info: Option, + stderr_tail: Option>, + report_source_file: Option, +} + +impl DossierBuilder { + pub fn new(channel_config: &ChannelConfig, ffmpeg_info: &FfmpegInfo) -> DossierBuilder { + DossierBuilder { + channel_config: channel_config.clone(), + pipeline: Pipeline { + ffmpeg_info: ffmpeg_info.clone(), + hw_accel: None, + }, + item_id: None, + item_json: None, + media_info: None, + stderr_tail: None, + report_source_file: None, + } + } + + pub fn item(mut self, item: &PlayoutItem) -> DossierBuilder { + self.item_id = Some(item.id.clone()); + self.item_json = + Some(serde_json::to_string_pretty(item).unwrap_or_else(|_| String::from("{}"))); + self + } + + pub fn stderr(mut self, stderr_tail: Vec) -> DossierBuilder { + self.stderr_tail = Some(stderr_tail); + self + } + + pub fn report_source(mut self, report_source_file: PathBuf) -> DossierBuilder { + self.report_source_file = Some(report_source_file); + self + } + + pub fn video(mut self, video_probe_result: &ProbeResult) -> DossierBuilder { + let value = serde_json::to_value(video_probe_result).unwrap_or(serde_json::Value::Null); + let mut media_info = self.media_info.unwrap_or(MediaInfo { + video: serde_json::Value::Null, + audio: serde_json::Value::Null, + subtitle: serde_json::Value::Null, + }); + media_info.video = value; + self.media_info = Some(media_info); + self + } + + pub fn audio(mut self, audio_probe_result: &ProbeResult) -> DossierBuilder { + let value = serde_json::to_value(audio_probe_result).unwrap_or(serde_json::Value::Null); + let mut media_info = self.media_info.unwrap_or(MediaInfo { + video: serde_json::Value::Null, + audio: serde_json::Value::Null, + subtitle: serde_json::Value::Null, + }); + media_info.audio = value; + self.media_info = Some(media_info); + + self + } + + pub fn subtitle(mut self, subtitle_probe_result: &ProbeResult) -> DossierBuilder { + let value = serde_json::to_value(subtitle_probe_result).unwrap_or(serde_json::Value::Null); + let mut media_info = self.media_info.unwrap_or(MediaInfo { + video: serde_json::Value::Null, + audio: serde_json::Value::Null, + subtitle: serde_json::Value::Null, + }); + media_info.subtitle = value; + self.media_info = Some(media_info); + + self + } + + pub fn accel(mut self, accel: &HardwareAccel) -> DossierBuilder { + self.pipeline.hw_accel = Some(accel.clone()); + self + } + + pub fn build(self) -> Dossier { + Dossier { + channel_config: self.channel_config, + pipeline: self.pipeline, + item_id: self.item_id, + item_json: self.item_json, + media_info: self.media_info, + stderr_tail: self.stderr_tail, + report_source_file: self.report_source_file, + } + } +} diff --git a/crates/ffpipeline/src/accel/amf.rs b/crates/ffpipeline/src/accel/amf.rs index 1ff2cc4..9039e00 100644 --- a/crates/ffpipeline/src/accel/amf.rs +++ b/crates/ffpipeline/src/accel/amf.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::ArgVec; use crate::ffmpeg_info::{FfmpegInfo, KnownHardwareAccel}; use crate::frame_size::FrameSize; @@ -6,7 +8,7 @@ use crate::pipeline::{FrameSurface, PixelFormat, SurfaceSet, VideoFormat}; use crate::probe::ProbeResultVideoStream; use crate::video_codec::VideoCodec; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct Amf; impl HwAccel for Amf { diff --git a/crates/ffpipeline/src/accel/cuda.rs b/crates/ffpipeline/src/accel/cuda.rs index 8a711d8..c99916e 100644 --- a/crates/ffpipeline/src/accel/cuda.rs +++ b/crates/ffpipeline/src/accel/cuda.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::ArgVec; use crate::capabilities::nvidia::NvidiaCapabilities; use crate::ffmpeg_info::{FfmpegInfo, KnownHardwareAccel, KnownVideoFilter}; @@ -14,7 +16,7 @@ use crate::video_filter::{ VideoFilter, VideoFilterOp, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct Cuda { pub capabilities: NvidiaCapabilities, } diff --git a/crates/ffpipeline/src/accel/qsv.rs b/crates/ffpipeline/src/accel/qsv.rs index 8dfdf4c..a1d19df 100644 --- a/crates/ffpipeline/src/accel/qsv.rs +++ b/crates/ffpipeline/src/accel/qsv.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::ArgVec; use crate::capabilities::qsv::QsvCapabilities; use crate::ffmpeg_info::{FfmpegInfo, KnownHardwareAccel, KnownVideoFilter}; @@ -9,7 +11,7 @@ use crate::probe::ProbeResultVideoStream; use crate::video_codec::VideoCodec; use crate::video_filter::{DeinterlaceFilter, ScaleFilter, VideoFilter, VideoFilterOp}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct Qsv { pub capabilities: QsvCapabilities, } diff --git a/crates/ffpipeline/src/accel/rkmpp.rs b/crates/ffpipeline/src/accel/rkmpp.rs index 71a0518..881ba93 100644 --- a/crates/ffpipeline/src/accel/rkmpp.rs +++ b/crates/ffpipeline/src/accel/rkmpp.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::ArgVec; use crate::capabilities::rkmpp::RkmppCapabilities; use crate::ffmpeg_info::{FfmpegInfo, KnownHardwareAccel}; @@ -7,7 +9,7 @@ use crate::pipeline::{FrameSurface, PixelFormat, SurfaceSet, VideoFormat}; use crate::probe::ProbeResultVideoStream; use crate::video_codec::VideoCodec; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct Rkmpp { pub capabilities: RkmppCapabilities, } diff --git a/crates/ffpipeline/src/accel/vaapi.rs b/crates/ffpipeline/src/accel/vaapi.rs index 17541fc..159e1b5 100644 --- a/crates/ffpipeline/src/accel/vaapi.rs +++ b/crates/ffpipeline/src/accel/vaapi.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::ArgVec; use crate::accel::opencl::{PadOpencl, TonemapOpencl}; use crate::capabilities::opencl::OpenCLCapabilities; @@ -18,7 +20,7 @@ use crate::video_filter::{ ToneMapFilter, VideoFilter, VideoFilterOp, }; -#[derive(Debug, Clone, PartialEq, strum::Display)] +#[derive(Debug, Clone, PartialEq, strum::Display, Serialize)] pub enum VaapiDriver { #[strum(serialize = "iHD")] Ihd, @@ -28,7 +30,7 @@ pub enum VaapiDriver { RadeonSI, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct Vaapi { pub device: String, pub driver: VaapiDriver, diff --git a/crates/ffpipeline/src/accel/video_toolbox.rs b/crates/ffpipeline/src/accel/video_toolbox.rs index fadee11..8397b57 100644 --- a/crates/ffpipeline/src/accel/video_toolbox.rs +++ b/crates/ffpipeline/src/accel/video_toolbox.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::ArgVec; use crate::capabilities::videotoolbox::VideoToolboxCapabilities; use crate::ffmpeg_info::{FfmpegInfo, KnownHardwareAccel, KnownVideoFilter}; @@ -9,7 +11,7 @@ use crate::probe::ProbeResultVideoStream; use crate::video_codec::VideoCodec; use crate::video_filter::{ScaleFilter, VideoFilter, VideoFilterOp}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct VideoToolbox { pub capabilities: VideoToolboxCapabilities, } diff --git a/crates/ffpipeline/src/accel/vulkan.rs b/crates/ffpipeline/src/accel/vulkan.rs index f362e95..ab1c0b5 100644 --- a/crates/ffpipeline/src/accel/vulkan.rs +++ b/crates/ffpipeline/src/accel/vulkan.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::ArgVec; use crate::capabilities::vulkan::VulkanCapabilities; use crate::ffmpeg_info::{FfmpegInfo, KnownHardwareAccel, KnownVideoFilter}; @@ -9,7 +11,7 @@ use crate::probe::ProbeResultVideoStream; use crate::video_codec::VideoCodec; use crate::video_filter::{ScaleFilter, ToneMapFilter, VideoFilter, VideoFilterOp}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct Vulkan { pub capabilities: VulkanCapabilities, } diff --git a/crates/ffpipeline/src/capabilities/nvidia/mod.rs b/crates/ffpipeline/src/capabilities/nvidia/mod.rs index 6fc43e4..055f55a 100644 --- a/crates/ffpipeline/src/capabilities/nvidia/mod.rs +++ b/crates/ffpipeline/src/capabilities/nvidia/mod.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use serde::Serialize; + use crate::pipeline::{PixelFormat, VideoFormat}; #[cfg(all( @@ -14,13 +16,13 @@ pub(crate) mod probe; )))] pub(crate) mod stub; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct EncoderCapability { pub bit_depths: Vec, pub b_frame_ref_mode: bool, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct NvidiaCapabilities { pub(crate) supported_decoders: HashMap>, pub(crate) supported_encoders: HashMap, diff --git a/crates/ffpipeline/src/capabilities/opencl/mod.rs b/crates/ffpipeline/src/capabilities/opencl/mod.rs index a3657ff..709ef85 100644 --- a/crates/ffpipeline/src/capabilities/opencl/mod.rs +++ b/crates/ffpipeline/src/capabilities/opencl/mod.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + #[cfg(all( any(target_os = "linux", target_os = "windows"), any(target_arch = "x86", target_arch = "x86_64") @@ -10,7 +12,7 @@ pub(crate) mod cl; )))] pub(crate) mod stub; -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Serialize)] pub struct OpenCLCapabilities { pub(crate) platform_count: u32, pub(crate) gpu_device_count: u32, diff --git a/crates/ffpipeline/src/capabilities/qsv/mod.rs b/crates/ffpipeline/src/capabilities/qsv/mod.rs index 2f03895..a4e920e 100644 --- a/crates/ffpipeline/src/capabilities/qsv/mod.rs +++ b/crates/ffpipeline/src/capabilities/qsv/mod.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Formatter}; use libvpl_sys::{MFX_FOURCC_NV12, MFX_FOURCC_P010}; +use serde::Serialize; use crate::pipeline::{PixelFormat, VideoFormat}; @@ -17,14 +18,14 @@ pub(crate) mod vpl; )))] pub(crate) mod stub; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct QsvCapabilities { pub(crate) supported_decoders: HashMap>, pub(crate) supported_encoders: HashMap>, pub(crate) vpp_pixel_formats: HashSet, } -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Clone, PartialEq, Eq, Hash, Serialize)] pub struct QsvPixelFormat(u32); impl Debug for QsvPixelFormat { diff --git a/crates/ffpipeline/src/capabilities/rkmpp/mod.rs b/crates/ffpipeline/src/capabilities/rkmpp/mod.rs index 4781989..c2e7726 100644 --- a/crates/ffpipeline/src/capabilities/rkmpp/mod.rs +++ b/crates/ffpipeline/src/capabilities/rkmpp/mod.rs @@ -1,5 +1,7 @@ use std::collections::HashSet; +use serde::Serialize; + use crate::pipeline::VideoFormat; #[cfg(all(target_os = "linux", target_arch = "aarch64"))] @@ -8,7 +10,7 @@ pub(crate) mod probe; #[cfg(not(all(target_os = "linux", target_arch = "aarch64")))] pub(crate) mod stub; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct RkmppCapabilities { pub(crate) supported_decoders: HashSet<(VideoFormat, u8)>, pub(crate) supported_encoders: HashSet<(VideoFormat, u8)>, diff --git a/crates/ffpipeline/src/capabilities/vaapi/mod.rs b/crates/ffpipeline/src/capabilities/vaapi/mod.rs index ec6c007..7d94d44 100644 --- a/crates/ffpipeline/src/capabilities/vaapi/mod.rs +++ b/crates/ffpipeline/src/capabilities/vaapi/mod.rs @@ -1,6 +1,8 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use libva_sys::*; +use serde::ser::SerializeMap; +use serde::{Serialize, Serializer}; use crate::pipeline::{PixelFormat, VideoFormat}; @@ -11,18 +13,23 @@ mod stub; type FourCC = u32; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct VaapiCapabilities { pub(crate) vendor: String, + #[serde(serialize_with = "serialize_profile_entrypoint_set")] pub(crate) supported: HashSet<(i32, i32)>, /// FourCC of supported pixel formats. + #[serde(serialize_with = "serialize_fourcc_set")] pub(crate) vpp_pixel_formats: HashSet, /// FourCC of supported HDR->SDR tonemap formats. + #[serde(serialize_with = "serialize_fourcc_set")] pub(crate) can_hdr_to_sdr_tonemap: HashSet, /// FourCC of supported HDR->HDR tonemap formats. + #[serde(serialize_with = "serialize_fourcc_set")] pub(crate) can_hdr_to_hdr_tonemap: HashSet, pub(crate) can_overlay: bool, /// Bitmask of VA_RC_* per (profile, entrypoint). Absent = unknown / not queried. + #[serde(serialize_with = "serialize_rate_control")] pub(crate) rate_control: HashMap<(i32, i32), u32>, } @@ -155,3 +162,92 @@ impl VaapiCapabilities { None } } + +fn fourcc_str(fourcc: u32) -> String { + String::from_utf8_lossy(&fourcc.to_le_bytes()) + .trim_end_matches('\0') + .to_owned() +} + +fn profile_name(p: i32) -> String { + match p { + VA_PROFILE_NONE => "None".into(), + VA_PROFILE_MPEG2_SIMPLE => "MPEG2Simple".into(), + VA_PROFILE_MPEG2_MAIN => "MPEG2Main".into(), + VA_PROFILE_MPEG4_SIMPLE => "MPEG4Simple".into(), + VA_PROFILE_MPEG4_ADVANCED_SIMPLE => "MPEG4AdvancedSimple".into(), + VA_PROFILE_MPEG4_MAIN => "MPEG4Main".into(), + VA_PROFILE_H264_MAIN => "H264Main".into(), + VA_PROFILE_H264_HIGH => "H264High".into(), + VA_PROFILE_VC1_SIMPLE => "VC1Simple".into(), + VA_PROFILE_VC1_MAIN => "VC1Main".into(), + VA_PROFILE_VC1_ADVANCED => "VC1Advanced".into(), + VA_PROFILE_H264_CONSTRAINED_BASELINE => "H264ConstrainedBaseline".into(), + VA_PROFILE_VP8_VERSION0_3 => "VP8Version0_3".into(), + VA_PROFILE_HEVC_MAIN => "HEVCMain".into(), + VA_PROFILE_HEVC_MAIN10 => "HEVCMain10".into(), + VA_PROFILE_VP9_PROFILE0 => "VP9Profile0".into(), + VA_PROFILE_VP9_PROFILE1 => "VP9Profile1".into(), + VA_PROFILE_VP9_PROFILE2 => "VP9Profile2".into(), + VA_PROFILE_VP9_PROFILE3 => "VP9Profile3".into(), + VA_PROFILE_AV1_PROFILE0 => "AV1Profile0".into(), + VA_PROFILE_AV1_PROFILE1 => "AV1Profile1".into(), + VA_PROFILE_H264_HIGH10 => "H264High10".into(), + _ => format!("Profile({p})"), + } +} + +fn entrypoint_name(e: i32) -> String { + match e { + VA_ENTRYPOINT_VLD => "VLD".into(), + VA_ENTRYPOINT_ENC_SLICE => "EncSlice".into(), + VA_ENTRYPOINT_ENC_SLICE_LP => "EncSliceLP".into(), + VA_ENTRYPOINT_VIDEO_PROC => "VideoProc".into(), + _ => format!("Entrypoint({e})"), + } +} + +fn rc_modes(mask: u32) -> Vec<&'static str> { + [(VA_RC_CQP, "CQP"), (VA_RC_CBR, "CBR"), (VA_RC_VBR, "VBR")] + .iter() + .filter_map(|(bit, name)| (mask & bit != 0).then_some(*name)) + .collect() +} + +fn serialize_fourcc_set(set: &HashSet, s: S) -> Result { + let mut v: Vec = set.iter().copied().map(fourcc_str).collect(); + v.sort(); + v.serialize(s) +} + +fn serialize_profile_entrypoint_set( + set: &HashSet<(i32, i32)>, + s: S, +) -> Result { + let mut grouped: BTreeMap> = BTreeMap::new(); + for &(p, e) in set { + grouped + .entry(profile_name(p)) + .or_default() + .push(entrypoint_name(e)); + } + for v in grouped.values_mut() { + v.sort(); + v.dedup(); + } + grouped.serialize(s) +} + +fn serialize_rate_control( + map: &HashMap<(i32, i32), u32>, + s: S, +) -> Result { + let mut sorted: Vec<_> = map.iter().collect(); + sorted.sort_by_key(|((p, e), _)| (*p, *e)); + let mut m = s.serialize_map(Some(sorted.len()))?; + for ((p, e), &mask) in sorted { + let key = format!("{}/{}", profile_name(*p), entrypoint_name(*e)); + m.serialize_entry(&key, &rc_modes(mask))?; + } + m.end() +} diff --git a/crates/ffpipeline/src/capabilities/videotoolbox/mod.rs b/crates/ffpipeline/src/capabilities/videotoolbox/mod.rs index 65c19e9..69bbe9a 100644 --- a/crates/ffpipeline/src/capabilities/videotoolbox/mod.rs +++ b/crates/ffpipeline/src/capabilities/videotoolbox/mod.rs @@ -1,5 +1,7 @@ use std::collections::HashSet; +use serde::Serialize; + use crate::pipeline::VideoFormat; #[cfg(target_os = "macos")] @@ -8,7 +10,7 @@ pub(crate) mod probe; #[cfg(not(target_os = "macos"))] pub(crate) mod stub; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct VideoToolboxCapabilities { pub(crate) supported_decoders: HashSet<(VideoFormat, u8)>, pub(crate) supported_encoders: HashSet<(VideoFormat, u8)>, diff --git a/crates/ffpipeline/src/capabilities/vulkan/mod.rs b/crates/ffpipeline/src/capabilities/vulkan/mod.rs index f0099db..e73b01f 100644 --- a/crates/ffpipeline/src/capabilities/vulkan/mod.rs +++ b/crates/ffpipeline/src/capabilities/vulkan/mod.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use serde::Serialize; + use crate::pipeline::VideoFormat; #[cfg(all( @@ -14,7 +16,7 @@ pub(crate) mod probe; )))] pub(crate) mod stub; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct VulkanCapabilities { pub(crate) device_index: u32, pub(crate) supported_decoders: HashMap>, diff --git a/crates/ffpipeline/src/ffmpeg_info.rs b/crates/ffpipeline/src/ffmpeg_info.rs index 878d052..380ba87 100644 --- a/crates/ffpipeline/src/ffmpeg_info.rs +++ b/crates/ffpipeline/src/ffmpeg_info.rs @@ -5,6 +5,7 @@ use std::iter::Iterator; use std::path::Path; use std::sync::LazyLock; +use serde::Serialize; use strum::{Display, EnumIter, IntoEnumIterator, IntoStaticStr}; use tokio::process::Command; @@ -88,7 +89,7 @@ pub enum KnownVideoFilter { YadifCuda, } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Serialize)] pub struct FfmpegInfo { pub(crate) hwaccels: HashSet, pub(crate) video_filters: HashSet, diff --git a/crates/ffpipeline/src/frame_rate.rs b/crates/ffpipeline/src/frame_rate.rs index 6fa9e25..ef9926e 100644 --- a/crates/ffpipeline/src/frame_rate.rs +++ b/crates/ffpipeline/src/frame_rate.rs @@ -1,4 +1,6 @@ -#[derive(Debug, Clone)] +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] pub struct FrameRate { pub r_frame_rate: String, pub parsed_frame_rate: f64, diff --git a/crates/ffpipeline/src/hw_accel.rs b/crates/ffpipeline/src/hw_accel.rs index 95e838b..8d78ae1 100644 --- a/crates/ffpipeline/src/hw_accel.rs +++ b/crates/ffpipeline/src/hw_accel.rs @@ -1,4 +1,5 @@ use enum_dispatch::enum_dispatch; +use serde::Serialize; use crate::ffmpeg_info::{FfmpegInfo, KnownHardwareAccel}; use crate::filter_chain::PipelineFilter; @@ -87,9 +88,10 @@ pub trait HwAccel { } } -#[derive(Debug, Clone, strum::Display)] +#[derive(Debug, Clone, strum::Display, Serialize)] #[enum_dispatch(HwAccel)] #[strum(serialize_all = "lowercase")] +#[serde(tag = "type")] pub enum HardwareAccel { Amf(accel::amf::Amf), Cuda(accel::cuda::Cuda), diff --git a/crates/ffpipeline/src/pipeline.rs b/crates/ffpipeline/src/pipeline.rs index fb9485f..e44ca8a 100644 --- a/crates/ffpipeline/src/pipeline.rs +++ b/crates/ffpipeline/src/pipeline.rs @@ -2,6 +2,7 @@ use std::fmt::Formatter; use std::path::PathBuf; use std::time::Duration; +use serde::Serialize; use strum::{Display, EnumString}; use crate::ArgVec; @@ -45,7 +46,7 @@ pub struct Kbps(pub u32); #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Hz(pub u32); -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString, Serialize)] #[strum(serialize_all = "lowercase")] pub enum VideoFormat { Av1, diff --git a/crates/ffpipeline/src/probe.rs b/crates/ffpipeline/src/probe.rs index c333bd3..117ef1d 100644 --- a/crates/ffpipeline/src/probe.rs +++ b/crates/ffpipeline/src/probe.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use std::time::Duration; use enum_dispatch::enum_dispatch; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use strum::EnumString; use tokio::process::Command; @@ -29,7 +29,7 @@ static SUBTITLE_IMAGE_CODECS: &[&str] = &[ static STILL_IMAGE_CODECS: &[&str] = &["png", "mjpeg", "bmp", "tiff"]; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct ProbeResultColorParams { pub color_range: Option, pub color_space: Option, @@ -45,7 +45,7 @@ impl ProbeResultColorParams { } } -#[derive(Debug, Clone, EnumString, PartialEq)] +#[derive(Debug, Clone, EnumString, PartialEq, Serialize)] #[strum(serialize_all = "lowercase")] pub enum CodecType { Audio, @@ -53,7 +53,7 @@ pub enum CodecType { Subtitle, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct ProbeResultVideoStream { pub stream_index: u32, pub codec: String, @@ -121,14 +121,14 @@ impl ProbeResultVideoStream { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct ProbeResultAudioStream { pub stream_index: u32, pub codec: String, pub channels: u32, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub enum ProbeResultStream { Video(Box), Audio(ProbeResultAudioStream), @@ -159,7 +159,7 @@ impl std::fmt::Display for ProbeResultStream { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct ProbeResult { pub path: String, pub streams: Vec, From 0f007e0beff0b16d62bfd5614d640bbe6afea619 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Sun, 24 May 2026 11:32:12 -0500 Subject: [PATCH 2/2] feedback --- crates/ersatztv-channel/src/dossier.rs | 30 ++++++-------------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/crates/ersatztv-channel/src/dossier.rs b/crates/ersatztv-channel/src/dossier.rs index 7a6a008..1c5c086 100644 --- a/crates/ersatztv-channel/src/dossier.rs +++ b/crates/ersatztv-channel/src/dossier.rs @@ -9,7 +9,7 @@ use ffpipeline::probe::ProbeResult; use serde::Serialize; use time::OffsetDateTime; -#[derive(Serialize)] +#[derive(Default, Serialize)] struct MediaInfo { video: serde_json::Value, audio: serde_json::Value, @@ -148,39 +148,21 @@ impl DossierBuilder { pub fn video(mut self, video_probe_result: &ProbeResult) -> DossierBuilder { let value = serde_json::to_value(video_probe_result).unwrap_or(serde_json::Value::Null); - let mut media_info = self.media_info.unwrap_or(MediaInfo { - video: serde_json::Value::Null, - audio: serde_json::Value::Null, - subtitle: serde_json::Value::Null, - }); - media_info.video = value; - self.media_info = Some(media_info); + self.media_info.get_or_insert_with(MediaInfo::default).video = value; self } pub fn audio(mut self, audio_probe_result: &ProbeResult) -> DossierBuilder { let value = serde_json::to_value(audio_probe_result).unwrap_or(serde_json::Value::Null); - let mut media_info = self.media_info.unwrap_or(MediaInfo { - video: serde_json::Value::Null, - audio: serde_json::Value::Null, - subtitle: serde_json::Value::Null, - }); - media_info.audio = value; - self.media_info = Some(media_info); - + self.media_info.get_or_insert_with(MediaInfo::default).audio = value; self } pub fn subtitle(mut self, subtitle_probe_result: &ProbeResult) -> DossierBuilder { let value = serde_json::to_value(subtitle_probe_result).unwrap_or(serde_json::Value::Null); - let mut media_info = self.media_info.unwrap_or(MediaInfo { - video: serde_json::Value::Null, - audio: serde_json::Value::Null, - subtitle: serde_json::Value::Null, - }); - media_info.subtitle = value; - self.media_info = Some(media_info); - + self.media_info + .get_or_insert_with(MediaInfo::default) + .subtitle = value; self }