feat: implementa funcionalidade de exportação de faixas de áudio e legenda

This commit is contained in:
2026-02-28 13:59:57 -03:00
parent a588ecd382
commit 4aa7cf2dd4
7 changed files with 334 additions and 12 deletions
+154
View File
@@ -0,0 +1,154 @@
use anyhow::{bail, Result};
use crate::application::ports::MediaProcessorPort;
use crate::domain::entities::{MediaTrackInfo, TrackKind};
use crate::domain::value_objects::FilePath;
/// Exporta uma faixa de áudio ou legenda já presente no arquivo de mídia para um arquivo separado.
///
/// O comando gerado delega todo o trabalho ao FFmpeg via `MediaProcessorPort`.
/// Nenhum reencoding é realizado para faixas de áudio (RNF-01 mantido).
/// Para legendas, o FFmpeg realiza apenas conversão de container de texto —
/// não há processamento de mídia envolvido.
pub struct ExportTrack;
impl ExportTrack {
/// Executa a exportação.
///
/// # Parâmetros
/// - `source` — caminho do arquivo de mídia de origem
/// - `track` — faixa a ser exportada (deve ser `Audio` ou `Subtitle`)
/// - `output` — caminho do arquivo de destino
/// - `processor` — port de execução do FFmpeg
///
/// # Erros
/// Retorna erro se a faixa for do tipo `Video` ou `Data`, ou se o FFmpeg falhar.
pub fn execute(
source: &FilePath,
track: &MediaTrackInfo,
output: &FilePath,
processor: &dyn MediaProcessorPort,
) -> Result<()> {
match track.kind {
TrackKind::Video | TrackKind::Data => {
bail!(
"Exportação não suportada para faixas do tipo '{}'. \
Apenas Áudio e Legenda podem ser exportados.",
track.kind
);
}
TrackKind::Audio | TrackKind::Subtitle => {}
}
use crate::adapters::ffmpeg::command_builder::FfmpegCommandBuilder;
let args = FfmpegCommandBuilder::build_export(source, track, output);
processor.execute(args)
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
use crate::application::ports::MediaProcessorPort;
use crate::domain::entities::{MediaTrackInfo, TrackKind};
use crate::domain::value_objects::{FilePath, TrackId};
use std::cell::RefCell;
struct MockProcessor {
captured_args: RefCell<Vec<String>>,
}
impl MediaProcessorPort for MockProcessor {
fn execute(&self, args: Vec<String>) -> Result<()> {
*self.captured_args.borrow_mut() = args;
Ok(())
}
}
fn audio_track() -> MediaTrackInfo {
MediaTrackInfo::new(
TrackId::new(1),
TrackKind::Audio,
"aac",
None,
1,
)
}
fn subtitle_track() -> MediaTrackInfo {
MediaTrackInfo::new(
TrackId::new(2),
TrackKind::Subtitle,
"subrip",
None,
2,
)
}
#[test]
fn exporta_audio_com_c_a_copy() {
let mock = MockProcessor { captured_args: RefCell::new(Vec::new()) };
let source = FilePath::from("video.mkv");
let output = FilePath::from("track.aac");
ExportTrack::execute(&source, &audio_track(), &output, &mock).unwrap();
let args = mock.captured_args.borrow();
assert!(args.contains(&"-c:a".to_string()), "faltou -c:a — args: {:?}", args);
assert!(args.contains(&"copy".to_string()), "faltou copy — args: {:?}", args);
// Não deve ter -c copy genérico junto com -c:a copy
let c_pos = args.iter().position(|a| a == "-c");
assert!(c_pos.is_none(), "não deve haver -c genérico para áudio — args: {:?}", args);
}
#[test]
fn exporta_audio_mapeia_stream_correto() {
let mock = MockProcessor { captured_args: RefCell::new(Vec::new()) };
let source = FilePath::from("video.mkv");
let output = FilePath::from("track.aac");
let track = audio_track(); // stream_index = 1
ExportTrack::execute(&source, &track, &output, &mock).unwrap();
let args = mock.captured_args.borrow();
let map_pos = args.iter().position(|a| a == "-map").expect("faltou -map");
assert_eq!(args[map_pos + 1], "0:1", "stream index incorreto — args: {:?}", args);
}
#[test]
fn exporta_legenda_sem_c_copy() {
let mock = MockProcessor { captured_args: RefCell::new(Vec::new()) };
let source = FilePath::from("video.mkv");
let output = FilePath::from("subtitle.srt");
ExportTrack::execute(&source, &subtitle_track(), &output, &mock).unwrap();
let args = mock.captured_args.borrow();
// Nenhuma forma de -c deve aparecer para legendas
assert!(!args.contains(&"-c:a".to_string()), "-c:a não deve aparecer para legenda");
assert!(!args.iter().any(|a| a == "-c"), "-c não deve aparecer para legenda — args: {:?}", args);
}
#[test]
fn exporta_legenda_output_e_ultimo_argumento() {
let mock = MockProcessor { captured_args: RefCell::new(Vec::new()) };
let source = FilePath::from("video.mkv");
let output = FilePath::from("subtitle.srt");
ExportTrack::execute(&source, &subtitle_track(), &output, &mock).unwrap();
let args = mock.captured_args.borrow();
assert_eq!(args.last().unwrap(), "subtitle.srt", "output deve ser o último arg");
}
#[test]
fn rejeita_faixa_de_video() {
let mock = MockProcessor { captured_args: RefCell::new(Vec::new()) };
let source = FilePath::from("video.mkv");
let output = FilePath::from("video_out.mkv");
let video_track = MediaTrackInfo::new(TrackId::new(0), TrackKind::Video, "h264", None, 0);
let result = ExportTrack::execute(&source, &video_track, &output, &mock);
assert!(result.is_err(), "deveria rejeitar faixa de vídeo");
}
}
+1
View File
@@ -2,6 +2,7 @@ pub mod add_audio_track;
pub mod add_subtitle;
pub mod adjust_sync;
pub mod edit_existing_track_sync;
pub mod export_track;
pub mod generate_output;
pub mod load_media_info;
pub mod set_track_language;