Manual do FFmpeg libav


Longo Procurei um livro que iria ser mastigados usar FFmpeg -como biblioteca conhecido como libav (o nome significa lib rary um udio v ideo ). Encontrou um livro " Como escrever um player de vídeo e caber em menos de mil linhas ". Infelizmente, as informações estão desatualizadas, então tive que criar um manual por conta própria.

A maior parte do código estará em C, mas não se preocupe: você pode entender e aplicá-lo facilmente no seu idioma favorito. O libF do FFmpeg possui muitas ligações para vários idiomas (incluindo Python e Go). Mas mesmo que seu idioma não tenha compatibilidade direta, você ainda pode se conectar via ffi (aqui está um exemplo comLua ).

Vamos começar com uma breve digressão sobre o que são vídeo, áudio, codecs e contêineres. Em seguida, prosseguimos para o curso intensivo sobre o uso da linha de comando FFmpeg e, finalmente, escrevemos o código. Sinta-se à vontade para ir direto para a seção "O caminho espinhoso para aprender o libav do FFmpeg".

Há uma opinião (e não apenas a minha) de que o streaming de vídeo na Internet já tomou o bastão da televisão tradicional. Seja como for, vale a pena explorar o libav do FFmpeg.

Índice


EDISON Software - desenvolvimento web
EDISON.

, , .

! ;-)


— , !


Se a sequência de imagens for alterada em uma determinada frequência (digamos, 24 imagens por segundo), é criada uma ilusão de movimento. Esta é a idéia principal do vídeo: uma série de imagens (quadros) movendo-se a uma determinada velocidade.

Ilustração de 1886.

Áudio é o que você ouve!


Embora o vídeo silencioso possa causar uma grande variedade de sentimentos, a adição de som aumenta drasticamente o grau de prazer.

O som é ondas vibracionais que se propagam no ar ou em qualquer outro meio de transmissão (como gás, líquido ou sólido).

Em um sistema de áudio digital, um microfone converte o som em um sinal elétrico analógico. Então, um conversor analógico-digital ( ADC ) - geralmente usando modulação por código de pulso ( PCM ) - converte um sinal analógico em digital.


Codec - compactação de dados


Um codec é um circuito eletrônico ou software que comprime ou descomprime o áudio / vídeo digital. Ele converte áudio / vídeo digital bruto (não compactado) em um formato compactado (ou vice-versa).

Mas se decidirmos agrupar milhões de imagens em um arquivo e chamá-lo de filme, podemos obter um arquivo enorme. Vamos calcular:

digamos que criamos um vídeo com uma resolução de 1080 × 1920 (altura × largura). Gastamos 3 bytes por pixel (o ponto mínimo na tela) para codificação de cores (cores de 24 bits, o que nos dá 16.777.216 cores diferentes). Este vídeo funciona a uma velocidade de 24 quadros por segundo, a duração total de 30 minutos.

toppf = 1080 * 1920 //    
cpp = 3 //  
tis = 30 * 60 //   
fps = 24 //   

required_storage = tis * fps * toppf * cpp

Este vídeo exigirá aproximadamente 250,28 GB de memória ou 1,11 Gb / s! É por isso que você precisa usar um codec.

Um contêiner é uma maneira conveniente de armazenar áudio / vídeo


O formato do contêiner (wrapper) é um formato de metarquivo cuja especificação descreve como vários elementos de dados e metadados coexistem em um arquivo de computador.

Este é um arquivo único que contém todos os fluxos (principalmente áudio e vídeo), fornecendo sincronização, contendo metadados comuns (como título, resolução) etc.

Normalmente, o formato do arquivo é determinado por sua extensão: por exemplo, video.webm é provavelmente um vídeo usando o contêiner de webm.


Linha de comando FFmpeg


Solução independente de plataforma cruzada para gravação, conversão e streaming de áudio / vídeo.

Para trabalhar com multimídia, temos uma ferramenta incrível - uma biblioteca chamada FFmpeg . Mesmo se você não o usar no código do programa, ainda o usará (você está usando o Chrome?).

A biblioteca possui um programa de console para inserir uma linha de comando chamada ffmpeg (em letras minúsculas, em contraste com o nome da própria biblioteca). Este é um binário simples e poderoso. Por exemplo, você pode converter de mp4 para avi digitando este comando:

$ ffmpeg -i input.mp4 output.avi

Nós apenas remixamos - convertidos de um contêiner para outro. Tecnicamente, o FFmpeg também pode transcodificar, mas mais sobre isso posteriormente.

Ferramenta de linha de comando FFmpeg 101


O FFmpeg possui documentação onde tudo é perfeitamente explicado como o que funciona.

Esquematicamente, o programa de linha de comando FFmpeg espera que o seguinte formato de argumento faça seu trabalho - ffmpeg {1} {2} -i {3} {4} {5}onde:

{1} - parâmetros globais
{2} - parâmetros do arquivo de entrada
{3} - URL de entrada
{4} - parâmetros do arquivo de saída
{5} - de saída As

partes do URL {2}, {3}, {4}, {5} especificam quantos argumentos são necessários. É mais fácil entender o formato de passar argumentos usando um exemplo:

AVISO: um arquivo por referência pesa 300 MB

$ wget -O bunny_1080p_60fps.mp4 http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_60fps_normal.mp4

$ ffmpeg \
-y \ #  
-c: libfdk_aac -c: v libx264 \ #  
-i bunny_1080p_60fps.mp4 \ #  URL
-c: v libvpx-vp9 -c: libvorbis \ #  
bunny_1080p_60fps_vp9.webm #  URL

Esse comando pega um arquivo mp4 de entrada contendo dois fluxos (áudio codificado usando o codec aac e vídeo codificado usando o codec h264) e o converte em webm, alterando também os codecs de áudio e vídeo.

Se você simplificar o comando acima, considere que o FFmpeg aceitará os valores padrão em vez de você. Por exemplo, se você simplesmente digitar

ffmpeg -i input.avi output.mp4

qual codec de áudio / vídeo ele usa para criar output.mp4?

Werner Robitz escreveu um guia de codificação e edição para leitura / execução com o FFmpeg.

Operações básicas de vídeo


Ao trabalhar com áudio / vídeo, geralmente executamos várias tarefas relacionadas à multimídia.

Transcodificação (transcodificação)




O que é isso? O processo de conversão de streaming ou áudio ou vídeo (ou ambos ao mesmo tempo) de um codec para outro. O formato do arquivo (contêiner) não muda.

Para quê? Ocorre que alguns dispositivos (TVs, smartphones, consoles etc.) não suportam o formato de áudio / vídeo X, mas suportam o formato de áudio / vídeo Y. Ou então, os codecs mais recentes são preferíveis porque oferecem uma melhor taxa de compactação.

Quão? Converta, por exemplo, vídeo H264 (AVC) em H265 (HEVC):

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c:v libx265 \
bunny_1080p_60fps_h265.mp4

Transmultiplexação



O que é isso? Converta de um formato (contêiner) para outro.

Para quê? Ocorre que alguns dispositivos (TVs, smartphones, consoles etc.) não oferecem suporte ao formato de arquivo X, mas ao formato de arquivo Y. Ou, contêineres mais novos, diferentemente dos mais antigos, fornecem as funções modernas necessárias.

Quão? Converta um mp4 para webm:

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c copy \ # just saying to ffmpeg to skip encoding
bunny_1080p_60fps.webm

Transrating



O que é isso? Altere a taxa de dados ou crie outra visão.

Para quê? O usuário pode assistir ao seu vídeo em uma rede 2G em um smartphone de baixa potência e via conexão de Internet de fibra óptica em uma TV 4K. Portanto, você deve oferecer mais de uma opção para reproduzir o mesmo vídeo com taxas de dados diferentes.

Quão? produz reprodução a uma taxa de bits entre 3856K e 2000K.

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-minrate 964K -maxrate 3856K -bufsize 2000K \
bunny_1080p_60fps_transrating_964_3856.mp4

Normalmente, o transrating é feito em conjunto com a recalibração. Werner Robitz escreveu outro artigo obrigatório sobre controle de velocidade FFmpeg.

Transizing (recalibração)



O que é isso? Alteração de resolução. Como afirmado acima, o transsizing é frequentemente realizado simultaneamente com o transrating.

Para quê? Pelas mesmas razões que com transrating.

Quão? Reduza a resolução de 1080 para 480:

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-vf scale=480:-1 \
bunny_1080p_60fps_transsizing_480.mp4

Bônus: streaming adaptável



O que é isso? Criação de muitas permissões (taxas de bits) e divisão da mídia em partes e sua transmissão através do protocolo http.

Para quê? Com o objetivo de fornecer multimídia flexível que pode ser visualizada mesmo em um smartphone econômico, mesmo em um plasma 4K, para que possa ser facilmente dimensionado e implantado (mas isso pode adicionar um atraso).

Quão? Crie WebM responsivo usando DASH:

# video streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 160x90 -b:v 250k -keyint_min 150 -g 150 -an -f webm -dash 1 video_160x90_250k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 320x180 -b:v 500k -keyint_min 150 -g 150 -an -f webm -dash 1 video_320x180_500k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 750k -keyint_min 150 -g 150 -an -f webm -dash 1 video_640x360_750k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 1000k -keyint_min 150 -g 150 -an -f webm -dash 1 video_640x360_1000k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 1280x720 -b:v 1500k -keyint_min 150 -g 150 -an -f webm -dash 1 video_1280x720_1500k.webm

# audio streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:a libvorbis -b:a 128k -vn -f webm -dash 1 audio_128k.webm

# the DASH manifest
$ ffmpeg \
 -f webm_dash_manifest -i video_160x90_250k.webm \
 -f webm_dash_manifest -i video_320x180_500k.webm \
 -f webm_dash_manifest -i video_640x360_750k.webm \
 -f webm_dash_manifest -i video_640x360_1000k.webm \
 -f webm_dash_manifest -i video_1280x720_500k.webm \
 -f webm_dash_manifest -i audio_128k.webm \
 -c copy -map 0 -map 1 -map 2 -map 3 -map 4 -map 5 \
 -f webm_dash_manifest \
 -adaptation_sets "id=0,streams=0,1,2,3,4 id=1,streams=5" \
 manifest.mpd

PS: Peguei este exemplo das instruções para reproduzir o Adaptive WebM usando DASH .

Indo além


Não há outros usos para o FFmpeg. Eu o uso com o iMovie para criar / editar alguns vídeos do YouTube. E, é claro, nada impede você de usá-lo profissionalmente.

O caminho espinhoso de aprender libf FFmpeg

Não é surpreendente, de tempos em tempos, percebido através da audição e da visão?

Biólogo David Robert Jones
O FFmpeg é extremamente útil como uma ferramenta de linha de comando para executar operações importantes com arquivos multimídia. Talvez também possa ser usado em programas?

O FFmpeg consiste em várias bibliotecas que podem ser integradas em nossos próprios programas. Normalmente, quando você instala o FFmpeg, todas essas bibliotecas são instaladas automaticamente. Vou me referir a um conjunto dessas bibliotecas como FFmpeg libav .

O título da seção é uma homenagem à série de Zed Shaw, The Thorny Path of Learning, [...] em particular seu livro The Thorny Path of Learning C.

Capítulo 0 - O Mundo Simples do Olá


No nosso Hello World , você realmente não aceita o mundo no idioma do console. Em vez disso, imprima as seguintes informações sobre o vídeo: formato (contêiner), duração, resolução, canais de áudio e, por fim, decodifique alguns quadros e salve-os como arquivos de imagem.

Arquitetura de libav FFmpeg


Mas antes de começarmos a escrever o código, vamos ver como a arquitetura libav do FFmpeg funciona em geral e como seus componentes interagem com os outros.

Aqui está um diagrama do processo de decodificação de vídeo:

Primeiro, o arquivo de mídia é carregado em um componente chamado AVFormatContext (o contêiner de vídeo também é um formato). De fato, ele não faz o download completo do arquivo inteiro: geralmente apenas o cabeçalho é lido.

Depois de baixar o cabeçalho mínimo do nosso contêiner , você pode acessar seus fluxos (eles podem ser representados como dados elementares de áudio e vídeo). Cada fluxo estará disponível no componente AVStream .

Suponha que nosso vídeo tenha dois fluxos: áudio codificado usando o codec AAC e vídeo codificado usando o codec H264 ( AVC ). De cada fluxo, podemos extrair pedaços de dados chamados pacotescarregados em componentes chamados AVPacket .

Os dados dentro dos pacotes ainda estão codificados (compactados) e, para decodificar os pacotes, precisamos passá-los para um AVCodec específico .

O AVCodec os decodifica em um AVFrame , como resultado desse componente nos fornece um quadro não compactado. Observe que a terminologia e o processo são os mesmos para os fluxos de áudio e vídeo.

Requisitos


Como às vezes há problemas ao compilar ou executar exemplos, usaremos o Docker como um ambiente de desenvolvimento / tempo de execução. Também usaremos um vídeo com um coelho grande ; portanto, se você não o tiver no computador local, basta executar o comando make fetch_small_bunny_video no console .

Na verdade, o código


TLDR mostre-me um exemplo de código executável, mano:

$ make run_hello

Omitiremos alguns detalhes, mas não se preocupe: o código fonte está disponível no github.

Vamos alocar memória para o componente AVFormatContext , que conterá informações sobre o formato (contêiner).

AVFormatContext *pFormatContext = avformat_alloc_context();

Agora vamos abrir o arquivo, ler o cabeçalho e preencher o AVFormatContext com informações mínimas sobre o formato (observe que os codecs geralmente não abrem). Para fazer isso, use a função avformat_open_input . Espera AVFormatContext , um nome de arquivo e dois argumentos opcionais: AVInputFormat (se você passar NULL, FFmpeg determinará o formato) e AVDictionary (que são opções de desmultiplexador).

avformat_open_input(&pFormatContext, filename, NULL, NULL);

Você também pode imprimir o nome do formato e a duração da mídia:

printf("Format %s, duration %lld us", pFormatContext->iformat->long_name, pFormatContext->duration);

Para acessar os fluxos, precisamos ler os dados da mídia. Isso é feito pela função avformat_find_stream_info . Agora pFormatContext-> nb_streams conterá o número de threads, e pFormatContext-> streams [i] nos fornecerá o i- ésimo fluxo em sequência ( AVStream ).

avformat_find_stream_info(pFormatContext,  NULL);

Vamos percorrer o loop em todos os threads:

for(int i = 0; i < pFormatContext->nb_streams; i++) {
  //
}

Para cada fluxo, salvaremos o AVCodecParameters , que descreve as propriedades do codec usado pelo i- ésimo fluxo:

AVCodecParameters *pLocalCodecParameters = pFormatContext->streams[i]->codecpar;


Usando as propriedades dos codecs, podemos encontrar o correspondente solicitando a função avcodec_find_decoder , também podemos encontrar o decodificador registrado para o identificador de codec e retornar o AVCodec , um componente que sabe como codificar e decodificar o fluxo:

AVCodec *pLocalCodec = avcodec_find_decoder(pLocalCodecParameters->codec_id);

Agora podemos imprimir as informações do codec:

// specific for video and audio
if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
  printf("Video Codec: resolution %d x %d", pLocalCodecParameters->width, pLocalCodecParameters->height);
} else if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_AUDIO) {
  printf("Audio Codec: %d channels, sample rate %d", pLocalCodecParameters->channels, pLocalCodecParameters->sample_rate);
}
// general
printf("\tCodec %s ID %d bit_rate %lld", pLocalCodec->long_name, pLocalCodec->id, pCodecParameters->bit_rate);

Usando o codec, alocamos memória para AVCodecContext , que conterá o contexto para o nosso processo de decodificação / codificação. Mas você precisa preencher esse contexto de codec com parâmetros CODEC - fazemos isso usando avcodec_parameters_to_context .

Depois de preencher o contexto do codec, você precisa abrir o codec. Chamamos a função avcodec_open2 e podemos usá-la:

AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
avcodec_parameters_to_context(pCodecContext, pCodecParameters);
avcodec_open2(pCodecContext, pCodec, NULL);

Agora vamos ler pacotes do fluxo e decodificá-los em quadros, mas primeiro precisamos alocar memória para os dois componentes ( AVPacket e AVFrame ).

AVPacket *pPacket = av_packet_alloc();
AVFrame *pFrame = av_frame_alloc();

Vamos alimentar nossos pacotes a partir dos fluxos da função av_read_frame enquanto ela tiver os pacotes:

while(av_read_frame(pFormatContext, pPacket) >= 0) {
  //...
}

Agora enviaremos o pacote de dados brutos (quadro compactado) para o decodificador através do contexto do codec usando a função avcodec_send_packet :

avcodec_send_packet(pCodecContext, pPacket);

E vamos obter um quadro de dados brutos (um quadro não compactado) do decodificador através do mesmo contexto de codec usando a função avcodec_receive_frame :

avcodec_receive_frame(pCodecContext, pFrame);

Podemos imprimir o número do quadro, PTS, DTS, tipo de quadro, etc.

printf(
    "Frame %c (%d) pts %d dts %d key_frame %d [coded_picture_number %d, display_picture_number %d]",
    av_get_picture_type_char(pFrame->pict_type),
    pCodecContext->frame_number,
    pFrame->pts,
    pFrame->pkt_dts,
    pFrame->key_frame,
    pFrame->coded_picture_number,
    pFrame->display_picture_number
);

E, finalmente, podemos salvar nosso quadro decodificado em uma simples imagem cinza. O processo é muito simples: usaremos pFrame-> data , onde o índice está associado aos espaços de cores Y , Cb e Cr . Basta selecionar 0 (Y) para salvar nossa imagem cinza:

save_gray_frame(pFrame->data[0], pFrame->linesize[0], pFrame->width, pFrame->height, frame_filename);

static void save_gray_frame(unsigned char *buf, int wrap, int xsize, int ysize, char *filename)
{
    FILE *f;
    int i;
    f = fopen(filename,"w");
    // writing the minimal required header for a pgm file format
    // portable graymap format -> https://en.wikipedia.org/wiki/Netpbm_format#PGM_example
    fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);

    // writing line by line
    for (i = 0; i < ysize; i++)
        fwrite(buf + i * wrap, 1, xsize, f);
    fclose(f);
}

E pronto! Agora temos uma imagem em escala de cinza de 2 MB:


Capítulo 1 - Sincronizar áudio e vídeo

Estar no jogo é quando um jovem desenvolvedor de JS grava um novo player de vídeo MSE.
Antes de começarmos a escrever o código de transcodificação, vamos falar sobre sincronização ou como o player de vídeo encontra o momento certo para reproduzir um quadro.

No exemplo anterior, salvamos vários quadros:


Quando projetamos um player de vídeo, precisamos reproduzir cada quadro em um determinado ritmo, caso contrário, é difícil apreciar o vídeo porque ele é reproduzido muito rápido ou muito lentamente.

Portanto, precisamos definir alguma lógica para uma reprodução suave de cada quadro. A este respeito, cada trama tem uma marca de representação de tempo ( PTS - a partir de p resentação t ime s tampo), que é um número crescente tidas em conta na variáveltimebase , que é um número racional (onde o denominador é conhecido como escala de tempo - escala de tempo ) dividido pela taxa de quadros ( fps ).

É mais fácil entender com exemplos. Vamos simular alguns cenários.

Para fps = 60/1 e timebase = 1/60000, cada PTS aumentará a escala de tempo / fps = 1000 , para que o tempo real de PTS para cada quadro possa ser (desde que comece em 0):

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1000, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2000, PTS_TIME = PTS * timebase = 0.033

Quase o mesmo cenário, mas com escala de tempo igual a 1/60:

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2, PTS_TIME = PTS * timebase = 0.033
frame=3, PTS = 3, PTS_TIME = PTS * timebase = 0.050

Para fps = 25/1 e timebase = 1/75, cada PTS aumentará a escala de tempo / fps = 3 e o tempo do PTS pode ser:

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 3, PTS_TIME = PTS * timebase = 0.04
frame=2, PTS = 6, PTS_TIME = PTS * timebase = 0.08
frame=3, PTS = 9, PTS_TIME = PTS * timebase = 0.12
...
frame=24, PTS = 72, PTS_TIME = PTS * timebase = 0.96
...
frame=4064, PTS = 12192, PTS_TIME = PTS * timebase = 162.56

Agora, com pts_time , podemos encontrar uma maneira de visualizar isso em sincronia com o som de pts_time ou com o relógio do sistema. O libF do FFmpeg fornece essas informações por meio de sua API:

fps = AVStream->avg_frame_rate
tbr = AVStream->r_frame_rate
tbn = AVStream->time_base


Por curiosidade, os quadros que salvamos foram enviados em ordem DTS (quadros: 1, 6, 4, 2, 3, 5), mas reproduzidos em ordem PTS (quadros: 1, 2, 3, 4, 5). Observe também quanto os quadros B mais baratos são comparados aos quadros P ou I :

LOG: AVStream->r_frame_rate 60/1
LOG: AVStream->time_base 1/60000
...
LOG: Frame 1 (type=I, size=153797 bytes) pts 6000 key_frame 1 [DTS 0]
LOG: Frame 2 (type=B, size=8117 bytes) pts 7000 key_frame 0 [DTS 3]
LOG: Frame 3 (type=B, size=8226 bytes) pts 8000 key_frame 0 [DTS 4]
LOG: Frame 4 (type=B, size=17699 bytes) pts 9000 key_frame 0 [DTS 2]
LOG: Frame 5 (type=B, size=6253 bytes) pts 10000 key_frame 0 [DTS 5]
LOG: Frame 6 (type=P, size=34992 bytes) pts 11000 key_frame 0 [DTS 1]

Capítulo 2 - Remultiplexação


Remultiplexagem (rearranjo, remuxing) - a transição de um formato (contêiner) para outro. Por exemplo, podemos substituir facilmente o vídeo MPEG-4 pelo MPEG-TS usando o FFmpeg:

ffmpeg input.mp4 -c copy output.ts

O arquivo MP4 será desmultiplexado, enquanto o arquivo não será decodificado ou codificado ( cópia -c ) e, no final, obteremos o arquivo mpegts. Se você não especificar o formato -f , o ffmpeg tentará adivinhar com base na extensão do arquivo.

O uso geral de FFmpeg ou libav segue esse padrão / arquitetura ou fluxo de trabalho:

  • nível de protocolo - aceitando dados de entrada (por exemplo, um arquivo, mas também pode ser download rtmp ou HTTP)
  • — , , ,
  • — (, ),
  • … :
  • — ( )
  • — ( ) ( )
  • — , , ( , , )


(Este gráfico é fortemente inspirado no trabalho de Leixiaohua e Slhck )

Agora vamos criar um exemplo usando libav para fornecer o mesmo efeito que ao executar este comando:

ffmpeg input.mp4 -c copy output.ts

Vamos ler da entrada ( input_format_context ) e alterá-la para outra saída ( output_format_context ):

AVFormatContext *input_format_context = NULL;
AVFormatContext *output_format_context = NULL;

Geralmente, começamos alocando memória e abrindo o formato de entrada. Para este caso específico, vamos abrir o arquivo de entrada e alocar memória para o arquivo de saída:

if ((ret = avformat_open_input(&input_format_context, in_filename, NULL, NULL)) < 0) {
  fprintf(stderr, "Could not open input file '%s'", in_filename);
  goto end;
}
if ((ret = avformat_find_stream_info(input_format_context, NULL)) < 0) {
  fprintf(stderr, "Failed to retrieve input stream information");
  goto end;
}

avformat_alloc_output_context2(&output_format_context, NULL, NULL, out_filename);
if (!output_format_context) {
  fprintf(stderr, "Could not create output context\n");
  ret = AVERROR_UNKNOWN;
  goto end;
}

Remultiplexaremos apenas fluxos de vídeo, áudio e legendas. Portanto, corrigimos quais fluxos usaremos em uma matriz de índices:

number_of_streams = input_format_context->nb_streams;
streams_list = av_mallocz_array(number_of_streams, sizeof(*streams_list));

Imediatamente após alocarmos a memória necessária, precisamos percorrer todos os fluxos e, para cada um deles, precisamos criar um novo fluxo de saída em nosso contexto do formato de saída usando a função avformat_new_stream . Observe que sinalizamos todos os fluxos que não são de vídeo, áudio ou legendas para que possamos ignorá-los.

for (i = 0; i < input_format_context->nb_streams; i++) {
  AVStream *out_stream;
  AVStream *in_stream = input_format_context->streams[i];
  AVCodecParameters *in_codecpar = in_stream->codecpar;
  if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
      in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
      in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
    streams_list[i] = -1;
    continue;
  }
  streams_list[i] = stream_index++;
  out_stream = avformat_new_stream(output_format_context, NULL);
  if (!out_stream) {
    fprintf(stderr, "Failed allocating output stream\n");
    ret = AVERROR_UNKNOWN;
    goto end;
  }
  ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
  if (ret < 0) {
    fprintf(stderr, "Failed to copy codec parameters\n");
    goto end;
  }
}

Agora crie o arquivo de saída:

if (!(output_format_context->oformat->flags & AVFMT_NOFILE)) {
  ret = avio_open(&output_format_context->pb, out_filename, AVIO_FLAG_WRITE);
  if (ret < 0) {
    fprintf(stderr, "Could not open output file '%s'", out_filename);
    goto end;
  }
}

ret = avformat_write_header(output_format_context, NULL);
if (ret < 0) {
  fprintf(stderr, "Error occurred when opening output file\n");
  goto end;
}

Depois disso, você pode copiar fluxos, pacote por pacote, de nossa entrada para nossos fluxos de saída. Isso acontece em um loop, desde que haja pacotes ( av_read_frame ), para cada pacote você precisa recalcular o PTS e o DTS para finalmente gravá-lo ( av_interleaved_write_frame ) em nosso contexto do formato de saída.

while (1) {
  AVStream *in_stream, *out_stream;
  ret = av_read_frame(input_format_context, &packet);
  if (ret < 0)
    break;
  in_stream  = input_format_context->streams[packet.stream_index];
  if (packet.stream_index >= number_of_streams || streams_list[packet.stream_index] < 0) {
    av_packet_unref(&packet);
    continue;
  }
  packet.stream_index = streams_list[packet.stream_index];
  out_stream = output_format_context->streams[packet.stream_index];
  /* copy packet */
  packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
  packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
  packet.duration = av_rescale_q(packet.duration, in_stream->time_base, out_stream->time_base);
  // https://ffmpeg.org/doxygen/trunk/structAVPacket.html#ab5793d8195cf4789dfb3913b7a693903
  packet.pos = -1;

  //https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html#ga37352ed2c63493c38219d935e71db6c1
  ret = av_interleaved_write_frame(output_format_context, &packet);
  if (ret < 0) {
    fprintf(stderr, "Error muxing packet\n");
    break;
  }
  av_packet_unref(&packet);
}

Para concluir, precisamos gravar o trailer do fluxo no arquivo de mídia de saída usando a função av_write_trailer :

av_write_trailer(output_format_context);

Agora estamos prontos para testar o código. E o primeiro teste será a conversão do formato (contêiner de vídeo) de MP4 para arquivo de vídeo MPEG-TS. Basicamente, criamos uma linha de comando ffmpeg input.mp4 -c para copiar output.ts usando o libav.

make run_remuxing_ts

Funciona! Não acredite em mim ?! Verifique com ffprobe :

ffprobe -i remuxed_small_bunny_1080p_60fps.ts

Input #0, mpegts, from 'remuxed_small_bunny_1080p_60fps.ts':
  Duration: 00:00:10.03, start: 0.000000, bitrate: 2751 kb/s
  Program 1
    Metadata:
      service_name    : Service01
      service_provider: FFmpeg
    Stream #0:0[0x100]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 60 fps, 60 tbr, 90k tbn, 120 tbc
    Stream #0:1[0x101]: Audio: ac3 ([129][0][0][0] / 0x0081), 48000 Hz, 5.1(side), fltp, 320 kb/s

Para resumir o que fizemos, agora podemos retornar à nossa idéia original de como o libav funciona. Mas perdemos parte do codec, que é exibido no diagrama.


Antes de terminar este capítulo, eu gostaria de mostrar uma parte tão importante do processo de remultiplexação, onde você pode passar parâmetros para o multiplexador. Suponha que você deseje fornecer o formato MPEG-DASH, portanto, é necessário usar mp4 fragmentado (às vezes chamado de fmp4 ) em vez de MPEG-TS ou MPEG-4 comum.

É fácil usar a linha de comando:

ffmpeg -i non_fragmented.mp4 -movflags frag_keyframe+empty_moov+default_base_moof fragmented.mp4

É quase tão simples quanto isso na versão libav, simplesmente passamos as opções ao escrever o cabeçalho de saída, imediatamente antes de copiar os pacotes:

AVDictionary* opts = NULL;
av_dict_set(&opts, "movflags", "frag_keyframe+empty_moov+default_base_moof", 0);
ret = avformat_write_header(output_format_context, &opts);

Agora podemos gerar esse arquivo mp4 fragmentado:

make run_remuxing_fragmented_mp4

Para garantir que tudo esteja correto, você pode usar o incrível site de ferramentas gpac / mp4box.js ou http://mp4parser.com/ para ver as diferenças - primeiro faça o download do mp4.

Como você pode ver, ele possui um bloco mdat indivisível - este é o local onde os quadros de vídeo e áudio estão localizados. Agora baixe o mp4 fragmentado para ver como ele estende os blocos mdat:

Capítulo 3 - Transcodificação


TLDR mostre-me o código e a execução:

$ make run_transcoding

Iremos pular alguns detalhes, mas não se preocupe: o código fonte está disponível no github.

Neste capítulo, criaremos um transcodificador minimalista escrito em C, que pode converter vídeos do H264 para o H265 usando as bibliotecas Favmpeg libav, em particular libavcodec , libavformat e libavutil .


AVFormatContext é uma abstração para o formato do arquivo de mídia, ou seja, para um contêiner (MKV, MP4, Webm, TS)
AVStream representa cada tipo de dado para um determinado formato (por exemplo: áudio, vídeo, legendas, metadados)
AVPacket é um fragmento de dados compactados recebidos do AVStream que podem ser decodificados usando o AVCodec (por exemplo : av1, h264, vp9, hevc) gerando dados brutos chamados AVFrame .

Transmultiplexação


Vamos começar com uma conversão simples e carregar o arquivo de entrada.

// Allocate an AVFormatContext
avfc = avformat_alloc_context();
// Open an input stream and read the header.
avformat_open_input(avfc, in_filename, NULL, NULL);
// Read packets of a media file to get stream information.
avformat_find_stream_info(avfc, NULL);

Agora configure o decodificador. O AVFormatContext nos dará acesso a todos os componentes do AVStream , e para cada um dos quais podemos obter seu AVCodec e criar um AVCodecContext específico . E, finalmente, podemos abrir esse codec para ir para o processo de decodificação.

AVCodecContext contém dados de configuração de mídia, como taxa de dados, taxa de quadros, taxa de amostragem, canais, afinação e muitos outros.

for(int i = 0; i < avfc->nb_streams; i++) {
  AVStream *avs = avfc->streams[i];
  AVCodec *avc = avcodec_find_decoder(avs->codecpar->codec_id);
  AVCodecContext *avcc = avcodec_alloc_context3(*avc);
  avcodec_parameters_to_context(*avcc, avs->codecpar);
  avcodec_open2(*avcc, *avc, NULL);
}

Você também precisa preparar o arquivo de mídia de saída para conversão. Primeiro, aloque memória para a saída AVFormatContext . Crie cada fluxo no formato de saída. Para compactar adequadamente o fluxo, copie os parâmetros do codec do decodificador.

Defina o sinalizador AV_CODEC_FLAG_GLOBAL_HEADER , que informa ao codificador que ele pode usar cabeçalhos globais e, finalmente, abra o arquivo de saída para gravar e salvar os cabeçalhos:

avformat_alloc_output_context2(&encoder_avfc, NULL, NULL, out_filename);

AVStream *avs = avformat_new_stream(encoder_avfc, NULL);
avcodec_parameters_copy(avs->codecpar, decoder_avs->codecpar);

if (encoder_avfc->oformat->flags & AVFMT_GLOBALHEADER)
  encoder_avfc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

avio_open(&encoder_avfc->pb, encoder->filename, AVIO_FLAG_WRITE);
avformat_write_header(encoder->avfc, &muxer_opts);

Nós get AVPacket do descodificador, ajustar a data e hora e escrever o pacote corretamente para o arquivo de saída. Apesar de a função av_interleaved_write_frame relatar " quadro de gravação ", salvamos o pacote. Concluímos o processo de permutação gravando o trailer do fluxo em um arquivo.

AVFrame *input_frame = av_frame_alloc();
AVPacket *input_packet = av_packet_alloc();

while(av_read_frame(decoder_avfc, input_packet) >= 0) {
  av_packet_rescale_ts(input_packet, decoder_video_avs->time_base, encoder_video_avs->time_base);
  av_interleaved_write_frame(*avfc, input_packet) < 0));
}

av_write_trailer(encoder_avfc);

Transcodificação


Na seção anterior, havia um programa simples para conversão, agora adicionaremos a capacidade de codificar arquivos, em particular, a transcodificação de vídeo de h264 para h265 .

Depois que o decodificador estiver preparado, mas antes de organizar o arquivo de mídia de saída, configure o codificador.

  • Crie um AVStream de vídeo no codificador avformat_new_stream .
  • Usamos o AVCodec com o nome libx265 , avcodec_find_encoder_by_name .
  • Crie um AVCodecContext com base no codec avcodec_alloc_context3 criado .
  • Defina os atributos básicos para uma sessão de transcodificação e ...
  • ... abra o codec e copie os parâmetros do contexto para o fluxo ( avcodec_open2 e avcodec_parameters_from_context ).

AVRational input_framerate = av_guess_frame_rate(decoder_avfc, decoder_video_avs, NULL);
AVStream *video_avs = avformat_new_stream(encoder_avfc, NULL);

char *codec_name = "libx265";
char *codec_priv_key = "x265-params";
// we're going to use internal options for the x265
// it disables the scene change detection and fix then
// GOP on 60 frames.
char *codec_priv_value = "keyint=60:min-keyint=60:scenecut=0";

AVCodec *video_avc = avcodec_find_encoder_by_name(codec_name);
AVCodecContext *video_avcc = avcodec_alloc_context3(video_avc);
// encoder codec params
av_opt_set(sc->video_avcc->priv_data, codec_priv_key, codec_priv_value, 0);
video_avcc->height = decoder_ctx->height;
video_avcc->width = decoder_ctx->width;
video_avcc->pix_fmt = video_avc->pix_fmts[0];
// control rate
video_avcc->bit_rate = 2 * 1000 * 1000;
video_avcc->rc_buffer_size = 4 * 1000 * 1000;
video_avcc->rc_max_rate = 2 * 1000 * 1000;
video_avcc->rc_min_rate = 2.5 * 1000 * 1000;
// time base
video_avcc->time_base = av_inv_q(input_framerate);
video_avs->time_base = sc->video_avcc->time_base;

avcodec_open2(sc->video_avcc, sc->video_avc, NULL);
avcodec_parameters_from_context(sc->video_avs->codecpar, sc->video_avcc);

É necessário expandir o ciclo de decodificação para transcodificar um fluxo de vídeo:

  • Enviamos um AVPacket vazio para o decodificador ( avcodec_send_packet ).
  • Obtenha o AVFrame descompactado ( avcodec_receive_frame ).
  • Começamos a recodificar o quadro bruto.
  • Enviamos o quadro bruto ( avcodec_send_frame ).
  • Obtemos compactação com base em nosso codec AVPacket ( avcodec_receive_packet ).
  • Defina o carimbo de data / hora ( av_packet_rescale_ts ).
  • Escrevemos no arquivo de saída ( av_interleaved_write_frame ).

AVFrame *input_frame = av_frame_alloc();
AVPacket *input_packet = av_packet_alloc();

while (av_read_frame(decoder_avfc, input_packet) >= 0)
{
  int response = avcodec_send_packet(decoder_video_avcc, input_packet);
  while (response >= 0) {
    response = avcodec_receive_frame(decoder_video_avcc, input_frame);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
      break;
    } else if (response < 0) {
      return response;
    }
    if (response >= 0) {
      encode(encoder_avfc, decoder_video_avs, encoder_video_avs, decoder_video_avcc, input_packet->stream_index);
    }
    av_frame_unref(input_frame);
  }
  av_packet_unref(input_packet);
}
av_write_trailer(encoder_avfc);

// used function
int encode(AVFormatContext *avfc, AVStream *dec_video_avs, AVStream *enc_video_avs, AVCodecContext video_avcc int index) {
  AVPacket *output_packet = av_packet_alloc();
  int response = avcodec_send_frame(video_avcc, input_frame);

  while (response >= 0) {
    response = avcodec_receive_packet(video_avcc, output_packet);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
      break;
    } else if (response < 0) {
      return -1;
    }

    output_packet->stream_index = index;
    output_packet->duration = enc_video_avs->time_base.den / enc_video_avs->time_base.num / dec_video_avs->avg_frame_rate.num * dec_video_avs->avg_frame_rate.den;

    av_packet_rescale_ts(output_packet, dec_video_avs->time_base, enc_video_avs->time_base);
    response = av_interleaved_write_frame(avfc, output_packet);
  }
  av_packet_unref(output_packet);
  av_packet_free(&output_packet);
  return 0;
}

Convertemos o fluxo de mídia de h264 para h265 . Como esperado, a versão do arquivo de mídia h265 é menor que o h264, enquanto o programa tem amplas oportunidades:

  /*
   * H264 -> H265
   * Audio -> remuxed (untouched)
   * MP4 - MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx265";
  sp.codec_priv_key = "x265-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> remuxed (untouched)
   * MP4 - MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> remuxed (untouched)
   * MP4 - fragmented MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";
  sp.muxer_opt_key = "movflags";
  sp.muxer_opt_value = "frag_keyframe+empty_moov+default_base_moof";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> AAC
   * MP4 - MPEG-TS
   */
  StreamingParams sp = {0};
  sp.copy_audio = 0;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";
  sp.audio_codec = "aac";
  sp.output_extension = ".ts";

  /* WIP :P  -> it's not playing on VLC, the final bit rate is huge
   * H264 -> VP9
   * Audio -> Vorbis
   * MP4 - WebM
   */
  //StreamingParams sp = {0};
  //sp.copy_audio = 0;
  //sp.copy_video = 0;
  //sp.video_codec = "libvpx-vp9";
  //sp.audio_codec = "libvorbis";
  //sp.output_extension = ".webm";

De coração, confesso que foi um pouco mais complicado do que parecia no começo. Eu tive que escolher o código fonte da linha de comando do FFmpeg e testar bastante. Provavelmente perdi algo em algum lugar, porque tive que usar o force-cfr para h264 , e algumas mensagens de aviso ainda aparecem, por exemplo, de que o tipo de quadro (5) foi forçado a ser alterado para o tipo de quadro (3).

Traduções no Blog Edison:


All Articles