feat: adiciona suporte para definir título e faixa padrão em faixas de áudio e legenda
This commit is contained in:
@@ -91,6 +91,18 @@ impl FfmpegCommandBuilder {
|
||||
ext_idx += 1;
|
||||
}
|
||||
|
||||
// ── Opções de muxing ─────────────────────────────────────────────────────
|
||||
// -max_interleave_delta 0: remove o limite do buffer de intercalação (padrão: 10s).
|
||||
// Sem isso, quando o áudio externo fica à frente do vídeo, o FFmpeg descarta
|
||||
// pacotes silenciosamente, gerando trechos mudos no output. Com 0, nenhum
|
||||
// pacote é descartado por excesso de delta — apenas memória RAM é consumida.
|
||||
// -avoid_negative_ts make_zero: normaliza timestamps negativos comuns em M4A/AAC
|
||||
// de serviços de streaming (DASH), que causam dessincronização no muxer MKV.
|
||||
args.push("-max_interleave_delta".to_string());
|
||||
args.push("0".to_string());
|
||||
args.push("-avoid_negative_ts".to_string());
|
||||
args.push("make_zero".to_string());
|
||||
|
||||
// ── -c copy (SEMPRE — invariante central do produto) ────────────────────
|
||||
args.push("-c".to_string());
|
||||
args.push("copy".to_string());
|
||||
@@ -115,16 +127,96 @@ impl FfmpegCommandBuilder {
|
||||
Track::Audio(t) => {
|
||||
args.push(format!("-metadata:s:a:{}", ext_audio_idx));
|
||||
args.push(format!("language={}", t.language));
|
||||
// Sempre emite title (mesmo vazio) para sobrescrever qualquer título
|
||||
// herdado do arquivo fonte (ex: "ISO Media file produced by Google Inc.").
|
||||
// Players como Jellyfin mobile usam o title para identificar a faixa.
|
||||
args.push(format!("-metadata:s:a:{}", ext_audio_idx));
|
||||
args.push(format!("title={}", t.title));
|
||||
ext_audio_idx += 1;
|
||||
}
|
||||
Track::Subtitle(t) => {
|
||||
args.push(format!("-metadata:s:s:{}", ext_sub_idx));
|
||||
args.push(format!("language={}", t.language));
|
||||
args.push(format!("-metadata:s:s:{}", ext_sub_idx));
|
||||
args.push(format!("title={}", t.title));
|
||||
ext_sub_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Disposições (faixa padrão) ────────────────────────────────────────
|
||||
// Quando o usuário marca uma faixa externa como "padrão", é necessário:
|
||||
// 1. Remover o flag `default` de todas as faixas existentes do mesmo tipo
|
||||
// (o arquivo fonte geralmente já tem `disposition:default=1` na sua
|
||||
// primeira faixa de áudio, e Jellyfin/Kodi respeitam esse flag).
|
||||
// 2. Emitir `-disposition:a/s:{idx} default` para a faixa externa marcada.
|
||||
//
|
||||
// Índices de disposição seguem a ordem de streams do tipo no output:
|
||||
// - Faixas existentes (existing_tracks) vêm primeiro, na ordem do map.
|
||||
// - Faixas externas (tracks) vêm depois, na ordem de adição.
|
||||
|
||||
let existing_audio_count_disp = project
|
||||
.existing_tracks
|
||||
.iter()
|
||||
.filter(|t| matches!(t.kind, TrackKind::Audio))
|
||||
.count();
|
||||
let existing_sub_count_disp = project
|
||||
.existing_tracks
|
||||
.iter()
|
||||
.filter(|t| matches!(t.kind, TrackKind::Subtitle))
|
||||
.count();
|
||||
|
||||
let any_ext_audio_default = project
|
||||
.tracks
|
||||
.iter()
|
||||
.any(|t| matches!(t, Track::Audio(a) if a.is_default));
|
||||
let any_ext_sub_default = project
|
||||
.tracks
|
||||
.iter()
|
||||
.any(|t| matches!(t, Track::Subtitle(s) if s.is_default));
|
||||
|
||||
// Remove default das faixas existentes quando uma faixa externa é padrão
|
||||
if any_ext_audio_default {
|
||||
for i in 0..existing_audio_count_disp {
|
||||
args.push(format!("-disposition:a:{}", i));
|
||||
args.push("0".to_string());
|
||||
}
|
||||
}
|
||||
if any_ext_sub_default {
|
||||
for i in 0..existing_sub_count_disp {
|
||||
args.push(format!("-disposition:s:{}", i));
|
||||
args.push("0".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Emite `default` para as faixas externas marcadas
|
||||
let mut disp_audio_idx = 0usize;
|
||||
let mut disp_sub_idx = 0usize;
|
||||
for track in &project.tracks {
|
||||
match track {
|
||||
Track::Audio(a) => {
|
||||
if a.is_default {
|
||||
args.push(format!(
|
||||
"-disposition:a:{}",
|
||||
existing_audio_count_disp + disp_audio_idx
|
||||
));
|
||||
args.push("default".to_string());
|
||||
}
|
||||
disp_audio_idx += 1;
|
||||
}
|
||||
Track::Subtitle(s) => {
|
||||
if s.is_default {
|
||||
args.push(format!(
|
||||
"-disposition:s:{}",
|
||||
existing_sub_count_disp + disp_sub_idx
|
||||
));
|
||||
args.push("default".to_string());
|
||||
}
|
||||
disp_sub_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Output ──────────────────────────────────────────────────────────────
|
||||
args.push(project.output.path.to_string_lossy().to_string());
|
||||
|
||||
@@ -216,6 +308,8 @@ mod tests {
|
||||
FilePath::from("audio.aac"),
|
||||
SyncOffset::from_ms(1200),
|
||||
TrackLanguage::new("por").unwrap(),
|
||||
false,
|
||||
String::new(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -233,6 +327,8 @@ mod tests {
|
||||
FilePath::from("audio.aac"),
|
||||
SyncOffset::default(),
|
||||
TrackLanguage::new("por").unwrap(),
|
||||
false,
|
||||
String::new(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -252,6 +348,8 @@ mod tests {
|
||||
FilePath::from("audio.aac"),
|
||||
SyncOffset::default(),
|
||||
TrackLanguage::new("por").unwrap(),
|
||||
false,
|
||||
String::new(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user