Skip to content

Commit

Permalink
feat: audio and subtitle track options
Browse files Browse the repository at this point in the history
  • Loading branch information
flazepe committed Oct 24, 2024
1 parent 4d2ef7a commit b5914a8
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 11 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ clipper -i input.mp4 -s 2:00-2:30 -s 5:12-5:20 -f output.mp4
clipper -i input.mp4 -s 2:00-2:30 -s 5:12-5:20 -f=1 output.mp4
```

Setting audio track and burning subtitles from input. This option works per input, like segments

```
clipper -input input.mp4 -audio-track 1 -subtitle-track 1 -segment 2:00-2:30 -segment 5:12-5:20 output.mp4
clipper -input input.mp4 -at 1 -st 1 -segment 2:00-2:30 -segment 5:12-5:20 output.mp4
```

Using NVENC with CQ

```
Expand Down
36 changes: 30 additions & 6 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::env::args as env_args;
/// A simple ffmpeg wrapper for clipping videos.
pub struct Args {
/// The input file
pub input: Vec<(String, Vec<String>)>,
pub input: Vec<Input>,

/// Whether to fade between segments. If set (e.g. "-fade=1"), this would be the fade duration in seconds (default: 0.5)
pub fade: Option<f64>,
Expand Down Expand Up @@ -50,6 +50,8 @@ impl Args {

match option {
"input" | "i" => current_option = Some(option.into()),
"audio-track" | "at" => current_option = Some(option.into()),
"subtitle-track" | "st" => current_option = Some(option.into()),
"segment" | "s" => current_option = Some(option.into()),
"fade" | "f" => {
fade = arg
Expand All @@ -70,10 +72,25 @@ impl Args {

if let Some(option) = &current_option {
match option.as_str() {
"input" | "i" => input.push((arg, vec![])),
"input" | "i" => input.push(Input {
file: arg,
segments: vec![],
audio_track: None,
subtitle_track: None,
}),
"audio-track" | "at" => {
if let Some(last_input) = input.last_mut() {
last_input.audio_track = Some(arg);
}
}
"subtitle-track" | "st" => {
if let Some(last_input) = input.last_mut() {
last_input.subtitle_track = Some(arg);
}
}
"segment" | "s" => {
if let Some((_, segments)) = input.last_mut() {
segments.push(arg);
if let Some(last_input) = input.last_mut() {
last_input.segments.push(arg);
}
}
"cq" => cq = Some(arg),
Expand All @@ -91,8 +108,8 @@ impl Args {
error!("Please enter at least one input.");
}

if let Some((input, _)) = input.iter().find(|input| input.1.is_empty()) {
error!(format!(r#"Input "{input}" has no segments."#));
if let Some(input) = input.iter().find(|input| input.segments.is_empty()) {
error!(format!(r#"Input "{}" has no segments."#, input.file));
}

if cq.as_ref().map_or(false, |cq| cq.parse::<f64>().is_err()) {
Expand All @@ -119,3 +136,10 @@ impl Args {
}
}
}

pub struct Input {
pub file: String,
pub segments: Vec<String>,
pub audio_track: Option<String>,
pub subtitle_track: Option<String>,
}
39 changes: 34 additions & 5 deletions src/clipper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ impl Clipper {
let mut filters = vec![];
let mut segment_count = 0;

for (input_index, (input, segments)) in self.0.input.iter().enumerate() {
args.append(&mut string_vec!["-i", input]);
for (input_index, input) in self.0.input.iter().enumerate() {
args.append(&mut string_vec!["-i", input.file]);

for segment in segments {
for segment in &input.segments {
let (from, to) = segment
.split_once('-')
.map(|(from, to)| (Self::duration_to_secs(from), Self::duration_to_secs(to)))
Expand All @@ -57,7 +57,18 @@ impl Clipper {
let fade_to = to - self.0.fade.unwrap_or(0.) - 0.5;

if !self.0.no_video {
let mut video_filters = vec![format!("[{input_index}:v]trim={from}:{to}")];
let video_label = match input.subtitle_track.as_ref() {
Some(subtitle_track) => {
let video_label = format!("v{input_index}s{subtitle_track}");
filters.push(format!(
r#"[{input_index}:v]subtitles={}:si={subtitle_track}[{video_label}]"#,
Self::escape_ffmpeg_chars(&input.file),
));
video_label
}
None => format!("{input_index}:v"),
};
let mut video_filters = vec![format!("[{video_label}]trim={from}:{to}")];
if let Some(fade) = self.0.fade {
video_filters.extend_from_slice(&[
format!("fade=t=in:st={from}:d={fade}"),
Expand All @@ -69,7 +80,9 @@ impl Clipper {
}

if !self.0.no_audio {
let mut audio_filters = vec![format!("[{input_index}:a]atrim={from}:{to}")];
let audio_track = input.audio_track.as_deref().unwrap_or("");
let mut audio_filters =
vec![format!("[{input_index}:a:{audio_track}]atrim={from}:{to}")];
if let Some(fade) = self.0.fade {
audio_filters.extend_from_slice(&[
format!("afade=t=in:st={from}:d={fade}"),
Expand Down Expand Up @@ -152,6 +165,22 @@ impl Clipper {
_ => 0.,
}
}

fn escape_ffmpeg_chars<T: Display>(string: T) -> String {
let mut chars = vec![];

for char in string.to_string().chars() {
if ['\'', '[', '\\', ']'].contains(&char) {
chars.extend_from_slice(&['\\', '\\', '\\', char, '\\', '\\', '\\']);
} else if char == ':' {
chars.extend_from_slice(&['\\', '\\', ':']);
} else {
chars.push(char);
}
}

chars.into_iter().collect()
}
}

#[macro_export]
Expand Down

0 comments on commit b5914a8

Please sign in to comment.