use std::{num::NonZeroUsize, path::PathBuf, time::Duration};
use orfail::OrFail;
use shiguredo_openh264::Openh264Library;
use crate::{
composer::Composer,
layout::{DEFAULT_LAYOUT_JSON, Layout},
stats::{ProcessorStats, Stats},
};
#[derive(Debug)]
struct Args {
layout_file_path: Option<PathBuf>,
output_file_path: Option<PathBuf>,
stats_file_path: Option<PathBuf>,
openh264: Option<PathBuf>,
no_progress_bar: bool,
worker_threads: NonZeroUsize,
root_dir: PathBuf,
}
impl Args {
fn parse(raw_args: &mut noargs::RawArgs) -> noargs::Result<Self> {
Ok(Self {
layout_file_path: noargs::opt("layout-file")
.short('l')
.ty("PATH")
.env("HISUI_LAYOUT_FILE_PATH")
.default("HISUI_REPO/layout-examples/compose-default.jsonc")
.doc("合成に使用するレイアウトファイルを指定します")
.take(raw_args)
.then(crate::arg_utils::parse_non_default_opt)?,
output_file_path: noargs::opt("output-file")
.short('o')
.ty("PATH")
.default("ROOT_DIR/output.mp4")
.doc("合成結果を保存するファイルを指定します")
.take(raw_args)
.then(crate::arg_utils::parse_non_default_opt)?,
stats_file_path: noargs::opt("stats-file")
.short('s')
.ty("PATH")
.doc("合成中に収集した統計情報 (JSON) を保存するファイルを指定します")
.take(raw_args)
.present_and_then(|a| a.value().parse())?,
openh264: noargs::opt("openh264")
.ty("PATH")
.env("HISUI_OPENH264_PATH")
.doc("OpenH264 の共有ライブラリのパスを指定します")
.take(raw_args)
.present_and_then(|a| a.value().parse())?,
no_progress_bar: noargs::flag("no-progress-bar")
.short('P')
.doc("指定された場合は、合成の進捗を非表示にします")
.take(raw_args)
.is_present(),
worker_threads: noargs::opt("thread-count")
.short('T')
.ty("INTEGER")
.default("1")
.env("HISUI_THREAD_COUNT")
.doc(concat!(
"合成処理に使用するワーカースレッド数を指定します\n",
"\n",
"なおこれはあくまでも Hisui 自体が起動するスレッドの数であり、\n",
"各エンコーダーやデコーダーが内部で起動するスレッドには関与しません",
))
.take(raw_args)
.then(|a| a.value().parse())?,
root_dir: noargs::arg("ROOT_DIR")
.example("/path/to/archive/RECORDING_ID/")
.doc(concat!(
"合成処理を行う際のルートディレクトリを指定します\n",
"\n",
"レイアウトファイル内に記載された相対パスの基点は、",
"このディレクトリとなります。\n",
"また、レイアウト内で、",
"このディレクトリの外のファイルが参照された場合にはエラーとなります。"
))
.take(raw_args)
.then(crate::arg_utils::validate_existing_directory_path)?,
})
}
}
pub fn run(mut raw_args: noargs::RawArgs) -> noargs::Result<()> {
let args = Args::parse(&mut raw_args)?;
if let Some(help) = raw_args.finish()? {
print!("{help}");
return Ok(());
}
let layout = Layout::from_layout_json_file_or_default(
args.root_dir.clone(),
args.layout_file_path.as_deref(),
DEFAULT_LAYOUT_JSON,
)
.or_fail()?;
log::debug!("layout: {layout:?}");
let openh264_lib = if let Some(path) = args.openh264.as_ref().filter(|_| layout.has_video()) {
Some(Openh264Library::load(path).or_fail()?)
} else {
None
};
let output_file_path = args
.output_file_path
.unwrap_or_else(|| args.root_dir.join("output.mp4"));
let mut composer = Composer::new(layout);
composer.openh264_lib = openh264_lib;
composer.show_progress_bar = !args.no_progress_bar;
composer.worker_threads = args.worker_threads;
composer.stats_file_path = args.stats_file_path;
let result = composer.compose(&output_file_path).or_fail()?;
if !result.success {
std::process::exit(1);
}
crate::json::pretty_print(nojson::json(|f| {
f.object(|f| {
if let Some(path) = &args.layout_file_path {
f.member("layout_file_path", path)?;
}
if let Some(path) = &composer.stats_file_path {
f.member("stats_file_path", path)?;
}
f.member("input_root_dir", &args.root_dir)?;
print_input_stats_summary(f, &result.stats)?;
f.member("output_file_path", &output_file_path)?;
print_output_stats_summary(f, &result.stats)?;
print_time_stats_summary(f, &result.stats)?;
Ok(())
})
}))
.or_fail()?;
Ok(())
}
fn print_input_stats_summary(
f: &mut nojson::JsonObjectFormatter<'_, '_, '_>,
stats: &Stats,
) -> std::fmt::Result {
let count = stats
.processors
.iter()
.filter(|s| {
matches!(
s,
ProcessorStats::WebmAudioReader(_) | ProcessorStats::Mp4AudioReader(_)
)
})
.count();
if count > 0 {
f.member("input_audio_source_count", count)?;
}
let count = stats
.processors
.iter()
.filter(|s| {
matches!(
s,
ProcessorStats::WebmVideoReader(_) | ProcessorStats::Mp4VideoReader(_)
)
})
.count();
if count > 0 {
f.member("input_video_source_count", count)?;
}
Ok(())
}
fn print_output_stats_summary(
f: &mut nojson::JsonObjectFormatter<'_, '_, '_>,
stats: &Stats,
) -> std::fmt::Result {
let Some(ProcessorStats::Mp4Writer(writer)) = stats
.processors
.iter()
.find(|x| matches!(x, ProcessorStats::Mp4Writer(_)))
else {
return Ok(());
};
if let Some(codec) = writer.audio_codec.get() {
f.member("output_audio_codec", codec)?;
for processor in &stats.processors {
if let ProcessorStats::AudioEncoder(encoder) = processor {
f.member("output_audio_encode_engine", encoder.engine)?;
break;
}
}
f.member(
"output_audio_duration_seconds",
writer.total_audio_track_duration.get().as_secs_f32(),
)?;
let duration = writer.total_audio_track_duration.get();
if !duration.is_zero() {
let bitrate = (writer.total_audio_sample_data_byte_size.get() as f32 * 8.0)
/ duration.as_secs_f32();
f.member("output_audio_bitrate", bitrate as u64)?;
}
}
if let Some(codec) = writer.video_codec.get() {
f.member("output_video_codec", codec)?;
for processor in &stats.processors {
if let ProcessorStats::VideoEncoder(encoder) = processor {
f.member("output_video_encode_engine", encoder.engine)?;
break;
}
}
f.member(
"output_video_duration_seconds",
writer.total_video_track_duration.get().as_secs_f32(),
)?;
let duration = writer.total_video_track_duration.get();
if !duration.is_zero() {
let bitrate = (writer.total_video_sample_data_byte_size.get() as f32 * 8.0)
/ duration.as_secs_f32();
f.member("output_video_bitrate", bitrate as u64)?;
}
}
for processor in &stats.processors {
match processor {
ProcessorStats::AudioMixer(_mixer) => {}
ProcessorStats::VideoMixer(mixer) => {
f.member("output_video_width", mixer.output_video_resolution.width)?;
f.member("output_video_height", mixer.output_video_resolution.height)?;
}
_ => {}
}
}
Ok(())
}
fn print_time_stats_summary(
f: &mut nojson::JsonObjectFormatter<'_, '_, '_>,
stats: &Stats,
) -> std::fmt::Result {
let total_audio_decoder_processing_duration = stats
.processors
.iter()
.filter_map(|decoder| match decoder {
ProcessorStats::AudioDecoder(audio_decoder) => {
Some(audio_decoder.total_processing_duration.get())
}
_ => None,
})
.sum::<Duration>();
if !total_audio_decoder_processing_duration.is_zero() {
f.member(
"total_audio_decoder_processing_seconds",
total_audio_decoder_processing_duration.as_secs_f64(),
)?;
}
let total_video_decoder_processing_duration = stats
.processors
.iter()
.filter_map(|decoder| match decoder {
ProcessorStats::VideoDecoder(video_decoder) => {
Some(video_decoder.total_processing_duration.get())
}
_ => None,
})
.sum::<Duration>();
if !total_video_decoder_processing_duration.is_zero() {
f.member(
"total_video_decoder_processing_seconds",
total_video_decoder_processing_duration.as_secs_f64(),
)?;
}
let total_audio_encoder_processing_duration = stats
.processors
.iter()
.filter_map(|encoder| match encoder {
ProcessorStats::AudioEncoder(audio_encoder) => {
Some(audio_encoder.total_processing_duration.get())
}
_ => None,
})
.sum::<Duration>();
if !total_audio_encoder_processing_duration.is_zero() {
f.member(
"total_audio_encoder_processing_seconds",
total_audio_encoder_processing_duration.as_secs_f64(),
)?;
}
let total_video_encoder_processing_duration = stats
.processors
.iter()
.filter_map(|encoder| match encoder {
ProcessorStats::VideoEncoder(video_encoder) => {
Some(video_encoder.total_processing_duration.get())
}
_ => None,
})
.sum::<Duration>();
if !total_video_encoder_processing_duration.is_zero() {
f.member(
"total_video_encoder_processing_seconds",
total_video_encoder_processing_duration.as_secs_f64(),
)?;
}
let total_audio_mixer_processing_duration = stats
.processors
.iter()
.filter_map(|mixer| match mixer {
ProcessorStats::AudioMixer(audio_mixer) => {
Some(audio_mixer.total_processing_duration.get())
}
_ => None,
})
.sum::<Duration>();
if !total_audio_mixer_processing_duration.is_zero() {
f.member(
"total_audio_mixer_processing_seconds",
total_audio_mixer_processing_duration.as_secs_f64(),
)?;
}
let total_video_mixer_processing_duration = stats
.processors
.iter()
.filter_map(|mixer| match mixer {
ProcessorStats::VideoMixer(video_mixer) => {
Some(video_mixer.total_processing_duration.get())
}
_ => None,
})
.sum::<Duration>();
if !total_video_mixer_processing_duration.is_zero() {
f.member(
"total_video_mixer_processing_seconds",
total_video_mixer_processing_duration.as_secs_f64(),
)?;
}
f.member("elapsed_seconds", stats.elapsed_duration.as_secs_f32())?;
Ok(())
}