Converter vídeos para HLS e MPEG-DASH com Rust
Use Rust para executar o FFmpeg, converter um vídeo local em HLS e MPEG-DASH e servir os arquivos concluídos por HTTP. Este tutorial cria uma representação HLS e duas representações de vídeo DASH, depois reproduz cada formato com o ffplay. O código Rust gerencia processos e arquivos; o FFmpeg faz a codificação, então você não precisa dos cabeçalhos de desenvolvimento do FFmpeg nem de bindings para Rust.
Entender a saída
O HLS usa playlists .m3u8; o DASH usa um manifesto XML .mpd.
Ambos descrevem segmentos de mídia que um player busca por HTTP. A reprodução com taxa de bits
adaptativa também exige várias representações compatíveis e um player que selecione entre elas
conforme as condições mudam.
O exemplo HLS empacota uma única codificação. O exemplo DASH oferece vídeo em 1920 × 1080 e 1280 × 720 com uma faixa de áudio compartilhada. Criar esses arquivos e reproduzi-los localmente demonstra o empacotamento e a reprodução, não a adaptação à largura de banda, a redução da latência de entrega nem um serviço de streaming em produção.
Configurar um projeto Rust
Use Bash no Linux, um conjunto de ferramentas estável do Rust instalado pelo
rustup e uma
distribuição do FFmpeg que contenha ffmpeg, ffprobe e
ffplay. Em algumas distribuições, o player ffplay vem em um pacote separado;
verifique sua disponibilidade antes de criar o projeto. A reprodução exige uma sessão de ambiente
gráfico e saída de áudio. O procedimento foi testado com Rust e Cargo 1.98.1 e FFmpeg, ffprobe e
ffplay 9.0.1. Essas são as versões testadas, não versões mínimas obrigatórias; use as verificações
de recursos abaixo ao avaliar outra versão que ainda receba manutenção.
No Ubuntu ou Debian, o pacote da distribuição é uma opção de instalação:
sudo apt-get update && sudo apt-get install -y ffmpeg
Verifique as versões e os recursos. Sua compilação precisa dos codificadores
libx264 e AAC, dos multiplexadores e demultiplexadores HLS e DASH e dos filtros
testsrc2, geq e aevalsrc usados pelo gerador da amostra.
cargo --version && rustc --version &&
ffmpeg -version && ffprobe -version && ffplay -version &&
ffmpeg -encoders && ffmpeg -muxers && ffmpeg -demuxers && ffmpeg -filters
Crie o projeto fora de um projeto Cargo existente ou do escopo de uma configuração
.cargo em um diretório ancestral.
O Cargo descobre configurações nos diretórios pai,
e a criação do projeto pode incluir um novo pacote em um workspace que o englobe. O bloco abaixo
verifica esse limite antes de criar arquivos e mantém o diretório e as opções do seu shell
inalterados:
(
set -eu
for tool in cargo rustc ffmpeg ffprobe ffplay; do
command -v "$tool" >/dev/null
done
directory=$(pwd -P)
while :; do
for config in "$directory/Cargo.toml" "$directory/.cargo/config" "$directory/.cargo/config.toml"; do
if [ -e "$config" ] || [ -L "$config" ]; then
printf 'Choose a directory outside this Cargo configuration: %s\n' "$config" >&2
exit 1
fi
done
[ "$directory" = / ] && break
directory=$(dirname "$directory")
done
cargo new --vcs none --edition 2021 video_converter
)
Se video_converter já existir, escolha um novo diretório em vez de excluir um projeto
existente. Todos os blocos de shell a seguir são executados a partir do diretório pai que contém
video_converter.
Substitua o video_converter/Cargo.toml gerado por este manifesto completo. As versões das
dependências diretas estão fixadas na API testada; mantenha o Cargo.lock gerado
para repetir as compilações.
[package]
name = "video_converter"
version = "0.1.0"
edition = "2021"
[workspace]
[dependencies]
anyhow = "=1.0.104"
tokio = { version = "=1.53.2", features = ["macros", "rt-multi-thread"] }
warp = { version = "=0.3.7", default-features = false }
Criar uma entrada conhecida
Gere um vídeo de 9,375 segundos em 1920 × 1080, a 24 quadros por segundo, com um tom de áudio ascendente. O padrão de teste em movimento tem oito blocos em preto e branco na parte superior; eles codificam o número do quadro, começando em zero. Isso facilita a identificação de conteúdo repetido ou fora de ordem. O subshell recusa uma entrada já existente antes de executar o FFmpeg:
(
set -eu
cd video_converter
if [ -e input.mp4 ] || [ -L input.mp4 ]; then
printf 'input.mp4 already exists; use a fresh project for this sample.\n' >&2
exit 1
fi
ffmpeg -hide_banner -loglevel error -nostdin -n \
-f lavfi -i 'testsrc2=size=1920x1080:rate=24:duration=9.375' \
-f lavfi -i 'aevalsrc=0.1*sin(2*PI*(440*t+40*t*t)):s=48000:d=9.375' \
-filter_threads 1 \
-vf "geq=lum='if(lt(Y,120),16+219*mod(floor(N/pow(2,floor(X/240))),2),lum(X,Y))':cb='cb(X,Y)':cr='cr(X,Y)'" \
-c:v libx264 -threads:v 2 -preset fast -crf 18 -pix_fmt yuv420p \
-c:a aac -b:a 128k -shortest input.mp4
)
A reprodução verificada usa essa amostra de 9,375 segundos. Para usar sua própria fonte, escolha
um MP4 local válido com vídeo 16:9 sem rotação, pixels quadrados, resolução de pelo menos
1920 × 1080 e uma faixa de áudio. Esse conjunto fixo de representações foi definido para essa
entrada; ele não escolhe tamanhos ou taxas de bits adequados a qualquer material. Apenas o primeiro
fluxo de vídeo e o primeiro fluxo de áudio são usados. O conversor normaliza o vídeo para
24 quadros por segundo e yuv420p. Verifique outras durações de clipe e outros
players separadamente.
Integrar o FFmpeg ao Rust
O Command do Rust passa argumentos
diretamente ao FFmpeg, sem um shell. O executor abaixo resolve o caminho canônico da entrada,
exige um novo diretório de saída e aguarda o término de cada processo filho. Ele só informa
sucesso depois que ambas as conversões terminam com sucesso e sem diagnósticos de nível de erro.
Implementar a conversão de vídeo para HLS e MPEG-DASH
Substitua video_converter/src/main.rs por este programa completo:
use anyhow::{ensure, Context, Result};
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;
const COMMON_ARGS: &[&str] = &[
"-hide_banner", "-loglevel", "error", "-nostdin", "-n", "-xerror",
"-threads", "2",
];
const ENCODING_ARGS: &[&str] = &[
"-c:v", "libx264", "-threads:v", "2", "-filter_threads", "1",
"-preset", "medium", "-profile:v", "main", "-pix_fmt", "yuv420p",
"-r", "24", "-g", "96", "-sc_threshold", "0", "-flags", "+cgop",
"-force_key_frames", "expr:gte(t,n_forced*4)",
"-c:a", "aac", "-b:a", "128k", "-ar", "48000",
];
const HLS_ARGS: &[&str] = &[
"-map", "0:v:0", "-map", "0:a:0",
"-crf", "23", "-f", "hls", "-hls_time", "4",
"-hls_playlist_type", "vod", "-hls_segment_filename", "segment_%03d.ts",
"index.m3u8",
];
const DASH_ARGS: &[&str] = &[
"-map", "0:v:0", "-map", "0:v:0", "-map", "0:a:0",
"-filter:v:0", "scale=1920:1080", "-filter:v:1", "scale=1280:720",
"-b:v:0", "2M", "-b:v:1", "1M",
"-f", "dash", "-seg_duration", "4", "-use_template", "1", "-use_timeline", "1",
"-format_options", "use_editlist=0",
"-init_seg_name", "init-$RepresentationID$.m4s",
"-media_seg_name", "chunk-$RepresentationID$-$Number%05d$.m4s",
"-adaptation_sets", "id=0,streams=v id=1,streams=a", "manifest.mpd",
];
fn convert(input: &Path, output: &Path, format_args: &[&str]) -> Result<()> {
std::fs::create_dir(output).context("Use a new output directory")?;
let result = Command::new("ffmpeg")
.args(COMMON_ARGS)
.arg("-i").arg(input)
.args(ENCODING_ARGS)
.args(format_args)
.current_dir(output)
.output()
.with_context(|| format!("Could not launch FFmpeg for {}", input.display()))?;
io::stderr().write_all(&result.stderr)?;
ensure!(result.status.success() && result.stderr.is_empty(),
"FFmpeg failed for {} ({})", input.display(), result.status);
Ok(())
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args_os().skip(1).collect();
ensure!(args.len() == 2,
"Usage: cargo run --bin video_converter -- INPUT.mp4 NEW_OUTPUT_DIR");
let input = std::fs::canonicalize(&args[0]).context("Cannot open input")?;
let output = Path::new(&args[1]);
std::fs::create_dir(output).context("Use a new output directory")?;
convert(&input, &output.join("hls"), HLS_ARGS)?;
convert(&input, &output.join("dash"), DASH_ARGS)?;
println!("Conversion complete.");
Ok(())
}
Converter para HLS
O HLS produz hls/index.m3u8 e segmentos MPEG-TS. Abra essa playlist de mídia
diretamente: um exemplo com uma única representação não precisa de uma playlist mestre. O CRF 23
seleciona o controle de taxa com qualidade constante, então a taxa de bits varia conforme o
material. Ele não define uma meta de 2 Mbit/s nem um limite de taxa de pico.
O multiplexador HLS faz os cortes nos quadros-chave após a duração alvo do segmento. A 24 quadros por segundo, o GOP fechado de 96 quadros e os quadros-chave forçados alinham os limites de quatro segundos. O segmento final da amostra é mais curto porque 9,375 segundos não é um múltiplo de quatro.
Converter para MPEG-DASH
Mapear o vídeo duas vezes cria representações distintas em 1080p e 720p em um único conjunto de
adaptação de vídeo; o áudio tem seu próprio conjunto. O
multiplexador DASH grava fragmentos de inicialização, fragmentos
de mídia e sua linha do tempo em dash/manifest.mpd.
A opção do contêiner MP4 use_editlist=0
desativa as listas de edição. No leitor DASH do FFmpeg testado, isso evita que o corte inicial do
AAC seja aplicado novamente a cada limite de fragmento e que amostras sejam descartadas entre
segmentos.
Os codificadores DASH usam metas ilustrativas de taxa média de bits de 2 Mbit/s e 1 Mbit/s. Eles não usam também o CRF, que selecionaria um modo diferente de controle de taxa no libx264. Esses valores não são limites rígidos de largura de banda nem uma recomendação de qualidade para todos os vídeos. Ambas as representações de vídeo usam a mesma programação de quadros-chave.
Criar um servidor de streaming simples em Rust
Crie o diretório para um executável separado do servidor:
mkdir video_converter/src/bin
Salve este programa completo como video_converter/src/bin/server.rs. Ele serve o diretório de saída
especificado pela interface de loopback e atribui tipos de conteúdo aos arquivos de streaming.
A mensagem de prontidão aparece após a vinculação bem-sucedida à porta; uma porta ocupada retorna
um erro em vez disso.
use anyhow::{ensure, Result};
use warp::{Filter, Reply};
#[tokio::main(worker_threads = 2)]
async fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
ensure!(args.len() == 2, "Usage: cargo run --bin server -- OUTPUT_DIR PORT");
let directory = std::fs::canonicalize(&args[0])?;
let port: u16 = args[1].parse()?;
let routes = warp::fs::dir(directory).map(|file: warp::fs::File| {
let content_type = match file.path().extension().and_then(|ext| ext.to_str()) {
Some("m3u8") => "application/vnd.apple.mpegurl",
Some("mpd") => "application/dash+xml",
Some("ts") => "video/mp2t",
Some("m4s") => "video/mp4",
_ => return file.into_response(),
};
let mut response = file.into_response();
response.headers_mut().insert(
warp::http::header::CONTENT_TYPE,
warp::http::HeaderValue::from_static(content_type),
);
response
});
let (address, server) = warp::serve(routes)
.try_bind_ephemeral(([127, 0, 0, 1], port))?;
println!("Serving http://{address}/");
server.await;
Ok(())
}
Tanto os fragmentos de áudio quanto os de vídeo DASH são contêineres MP4; o MPD descreve qual faixa cada um contém. O servidor e o player usam a mesma origem local, então este exemplo não precisa de uma configuração permissiva de CORS. Ele não tem endpoint de upload nem autenticação.
Testar a configuração de streaming
A partir do diretório pai, compile e execute o conversor; depois, inicie o servidor somente se a conversão for bem-sucedida:
(
cd video_converter &&
cargo run --bin video_converter -- ./input.mp4 ./output &&
cargo run --bin server -- ./output 3030
)
O conversor exibe Conversion complete. Em seguida, o servidor exibe sua URL. Se a porta 3030 estiver ocupada, escolha outra porta no comando do servidor e use-a nas URLs abaixo. Não execute outro conversor usando o mesmo diretório enquanto ele estiver sendo servido.
Em um segundo terminal, reproduza ambos os manifestos com o player ffplay instalado. O primeiro player encerra ao chegar ao fim, antes que o segundo seja iniciado:
ffplay -autoexit -threads 2 http://127.0.0.1:3030/hls/index.m3u8 &&
ffplay -autoexit -threads 2 http://127.0.0.1:3030/dash/manifest.mpd
Cada reprodução deve mostrar o mesmo padrão em movimento e blocos que mudam por cerca de
9,375 segundos, com um tom que sobe continuamente. A entrada gerada tem 225 quadros de vídeo.
O DASH anuncia as representações de vídeo em 1920 × 1080 e 1280 × 720; o ffplay escolhe um fluxo de
vídeo, então essa reprodução não demonstra a alternância entre elas. Encerre um player com
q. Pare o servidor em primeiro plano com Ctrl+C no terminal dele;
os arquivos de saída permanecem no disco.
Um segmento ausente pode interromper a reprodução mesmo quando sua playlist carrega. Mantenha toda
a árvore output junta e inspecione o vídeo e o áudio reais até o fim dos dois
fluxos. Carregar apenas um manifesto não demonstra que ele referencia o conteúdo correto.
Tratar uma falha na conversão
Se o diretório de saída já existir, a operação falha sem substituí-lo. Um arquivo ausente, uma faixa de áudio ausente, uma falha ao iniciar o FFmpeg, um código de término nativo diferente de zero ou um erro de decodificação reportado impedem a mensagem de conclusão e a execução encadeada do comando do servidor. Às vezes, o FFmpeg consegue recuperar mídia danificada sem reportar um erro; uma conversão bem-sucedida não prova que uma fonte qualquer estava intacta.
Uma falha após o início da codificação pode deixar arquivos parciais, incluindo uma conversão HLS
concluída quando a conversão DASH falha. Não sirva esse diretório. Preserve-o para inspeção e tente
executar o conversor novamente com um novo nome de saída, como ./output-retry;
após o sucesso, inicie o servidor com esse mesmo nome.
Se o download das dependências ou a compilação falhar, mantenha o projeto e execute novamente o
comando de compilação após corrigir a conectividade ou o erro do compilador. Os comandos do Cargo
compilam antes de executar, então um executável antigo não é usado após uma falha na compilação.
Mantenha este fluxo de trabalho voltado à conversão e reprodução locais. Para uma tarefa separada em Rust envolvendo vários processos FFmpeg ao mesmo tempo, veja aplicação simultânea de marcas-d’água em vídeos com Rust.
