Manual libav FFmpeg


Lama saya mencari buku yang akan dikunyah menggunakan perpustakaan seperti FFmpeg yang dikenal sebagai libav (nama singkatan dari lib rary a udio v ideo ). Menemukan buku teks " Cara menulis pemutar video dan muat dalam kurang dari seribu baris ." Sayangnya, informasi di sana sudah ketinggalan zaman, jadi saya harus membuat manual sendiri.

Sebagian besar kode berada dalam C, tetapi jangan khawatir: Anda akan dengan mudah memahami semuanya dan dapat menerapkannya dalam bahasa favorit Anda. Libav FFmpeg memiliki banyak ikatan ke banyak bahasa (termasuk Python dan Go). Tetapi bahkan jika bahasa Anda tidak memiliki kompatibilitas langsung, Anda masih dapat terhubung melalui ffi (di sini adalah contoh denganLua ).

Mari kita mulai dengan penyimpangan singkat tentang apa itu video, audio, codec dan wadah. Kemudian kita beralih ke kursus kilat tentang menggunakan baris perintah FFmpeg, dan akhirnya menulis kode. Jangan ragu untuk langsung ke bagian "The Thorny Path to Learning FFmpeg libav".

Ada pendapat (dan bukan hanya milik saya) bahwa streaming video Internet telah mengambil tongkat komando dari televisi tradisional. Namun, FFmpeg libav pasti layak untuk dijelajahi.

Daftar Isi


Perangkat Lunak EDISON - pengembangan web
EDISON.

, , .

! ;-)

↑


β€” , ! ↑


Jika urutan gambar diubah pada frekuensi yang diberikan (katakanlah, 24 gambar per detik), ilusi gerakan dibuat. Ini adalah ide utama video: serangkaian gambar (bingkai) bergerak dengan kecepatan tertentu.

Ilustrasi 1886.

Audio adalah apa yang Anda dengar! ↑


Meskipun video bisu dapat menyebabkan berbagai macam perasaan, menambahkan suara secara dramatis meningkatkan tingkat kesenangan.

Suara adalah gelombang getaran yang merambat di udara atau media transmisi lainnya (seperti gas, cairan atau padat).

Dalam sistem audio digital, mikrofon mengubah suara menjadi sinyal listrik analog. Kemudian, konverter analog-ke-digital ( ADC ) - biasanya menggunakan modulasi kode pulsa ( PCM ) - mengubah sinyal analog menjadi digital.


Codec - kompresi data ↑


Codec adalah sirkuit elektronik atau perangkat lunak yang mengompresi atau mendekompresi audio / video digital. Ini mengubah audio / video digital mentah (tidak terkompresi) menjadi format terkompresi (atau sebaliknya).

Tetapi jika kita memutuskan untuk mengemas jutaan gambar ke dalam satu file dan menyebutnya film, kita bisa mendapatkan file yang sangat besar. Mari kita hitung:

Katakanlah kita membuat video dengan resolusi 1080 Γ— 1920 (tinggi Γ— lebar). Kami menghabiskan 3 byte per piksel (titik minimum pada layar) untuk pengkodean warna (warna 24-bit, yang memberi kami 16.777.216 warna berbeda). Video ini bekerja pada kecepatan 24 frame per detik, total durasi 30 menit.

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

required_storage = tis * fps * toppf * cpp

Video ini akan membutuhkan sekitar 250,28 GB memori, atau 1,11 Gb / s! Itu sebabnya Anda harus menggunakan codec.

Wadah adalah cara mudah untuk menyimpan audio / video ↑


Format wadah (pembungkus) adalah format metafile yang spesifikasinya menjelaskan bagaimana berbagai data dan elemen metadata hidup berdampingan dalam file komputer.

Ini adalah file tunggal yang berisi semua aliran (terutama audio dan video), menyediakan sinkronisasi, berisi metadata umum (seperti judul, resolusi), dll.

Biasanya, format file ditentukan oleh ekstensi: misalnya, video.webm kemungkinan besar adalah video menggunakan wadah webm.


Baris perintah FFmpeg ↑


Solusi lintas platform mandiri untuk merekam, mengkonversi, dan streaming audio / video.

Untuk bekerja dengan multimedia, kami memiliki alat yang luar biasa - perpustakaan yang disebut FFmpeg . Bahkan jika Anda tidak menggunakannya dalam kode program Anda, Anda masih menggunakannya (apakah Anda menggunakan Chrome?).

Perpustakaan memiliki program konsol untuk memasukkan baris perintah yang disebut ffmpeg (dalam huruf kecil, berbeda dengan nama perpustakaan itu sendiri). Ini adalah biner sederhana dan kuat. Misalnya, Anda dapat mengonversi dari mp4 ke avi hanya dengan mengetik perintah ini:

$ ffmpeg -i input.mp4 output.avi

Kami baru saja remix - dikonversi dari satu wadah ke wadah lain. Secara teknis, FFmpeg juga bisa transcode, tetapi lebih lanjut tentang itu nanti.

Alat baris perintah FFmpeg 101 ↑


FFmpeg memiliki dokumentasi di mana semuanya dengan sempurna menjelaskan cara kerjanya.

Secara skematis, program baris perintah FFmpeg mengharapkan format argumen berikut untuk melakukan tugasnya - di ffmpeg {1} {2} -i {3} {4} {5}mana:

{1} - parameter global
{2} - parameter file input
{3} - URL masuk
{4} - parameter file output
{5} - keluar

Bagian URL {2}, {3}, {4}, {5} tentukan argumen sebanyak yang diperlukan. Lebih mudah untuk memahami format argumen yang lewat menggunakan contoh:

PERINGATAN: file dengan referensi berbobot 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

Perintah ini mengambil file mp4 yang masuk yang berisi dua aliran (audio disandikan menggunakan aac codec, dan video disandikan menggunakan codec h264), dan mengubahnya menjadi webm, mengubah juga codec audio dan video.

Jika Anda menyederhanakan perintah di atas, Anda harus mempertimbangkan bahwa FFmpeg akan menerima nilai default, bukan Anda. Misalnya, jika Anda cukup mengetik

ffmpeg -i input.avi output.mp4

codec audio / video mana yang digunakan untuk membuat output.mp4?

Werner Robitz menulis panduan pengodean dan pengeditan untuk membaca / mengeksekusi dengan FFmpeg.

Operasi video dasar ↑


Saat bekerja dengan audio / video, kami biasanya melakukan sejumlah tugas yang berkaitan dengan multimedia.

Transcoding (transcoding) ↑




Apa itu? Proses konversi streaming atau audio atau video (atau keduanya pada saat yang sama) dari satu codec ke yang lain. Format file (wadah) tidak berubah.

Untuk apa? Kebetulan beberapa perangkat (TV, smartphone, konsol, dll.) Tidak mendukung format audio / video X, tetapi mendukung format audio / video Y. Atau, codec yang lebih baru lebih disukai karena mereka memberikan rasio kompresi yang lebih baik.

Bagaimana? Konversi, misalnya, video H264 (AVC) ke H265 (HEVC):

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

Transmultiplexing ↑



Apa itu? Konversi dari satu format (wadah) ke yang lain.

Untuk apa? Kebetulan beberapa perangkat (TV, smartphone, konsol, dll.) Tidak mendukung format file X, tetapi mendukung format file Y. Atau, wadah yang lebih baru, tidak seperti yang lebih lama, menyediakan fungsi yang diperlukan modern.

Bagaimana? Konversi mp4 ke webm:

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

Transisi ↑



Apa itu? Ubah kecepatan data atau buat tampilan lain.

Untuk apa? Pengguna dapat menonton video Anda di jaringan 2G di smartphone berdaya rendah, dan melalui koneksi internet serat optik di TV 4K. Karena itu, Anda harus menawarkan lebih dari satu opsi untuk memutar video yang sama dengan kecepatan data yang berbeda.

Bagaimana? menghasilkan pemutaran pada laju bit antara 3856K dan 2000K.

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

Biasanya, pengubahan dilakukan bersamaan dengan kalibrasi ulang. Werner Robitz menulis artikel wajib lain tentang kontrol kecepatan FFmpeg.

Transizing (kalibrasi ulang) ↑



Apa itu? Resolusi berubah. Seperti yang dinyatakan di atas, transsizing sering dilakukan bersamaan dengan transrating.

Untuk apa? Untuk alasan yang sama dengan transrating.

Bagaimana? Kurangi resolusi 1080 hingga 480:

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

Bonus: streaming adaptif ↑



Apa itu? Pembuatan banyak izin (bitrate) dan pemisahan media menjadi beberapa bagian dan transmisinya melalui protokol http.

Untuk apa? Demi menyediakan multimedia yang fleksibel, yang dapat dilihat bahkan pada smartphone anggaran, bahkan pada plasma 4K, sehingga dapat dengan mudah ditingkatkan dan digunakan (tetapi ini dapat menambah penundaan).

Bagaimana? Buat WebM responsif menggunakan 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: Saya mengambil contoh ini dari instruksi untuk bermain Adaptive WebM menggunakan DASH .

Melampaui ↑


Tidak ada kegunaan lain untuk FFmpeg. Saya menggunakannya dengan iMovie untuk membuat / mengedit beberapa video YouTube. Dan, tentu saja, tidak ada yang mencegah Anda menggunakannya secara profesional.

Jalan yang sulit untuk belajar FFmpeg libav ↑

Bukankah menakjubkan dari waktu ke waktu yang dirasakan melalui pendengaran dan penglihatan?

Ahli biologi David Robert Jones
FFmpeg sangat berguna sebagai alat baris perintah untuk melakukan operasi penting dengan file multimedia. Mungkin bisa digunakan dalam program juga?

FFmpeg terdiri dari beberapa perpustakaan yang dapat diintegrasikan ke dalam program kami sendiri. Biasanya, ketika Anda menginstal FFmpeg, semua pustaka ini diinstal secara otomatis. Saya akan merujuk ke satu set perpustakaan ini sebagai FFmpeg libav .

Judul bagian ini merupakan penghargaan untuk seri Zed Shaw, The Thorny Path of Learning [...] , khususnya bukunya The Thorny Path of Learning C.

Bab 0 - Hello World Sederhana ↑


Di Hello World kami , Anda benar-benar tidak akan menyambut dunia dalam bahasa konsol. Sebagai gantinya, cetak informasi berikut tentang video: format (wadah), durasi, resolusi, saluran audio, dan akhirnya, dekripsi beberapa bingkai dan simpan sebagai file gambar.

Arsitektur lmpav FFmpeg ↑


Tetapi sebelum kita mulai menulis kode, mari kita lihat bagaimana arsitektur libav FFmpeg bekerja secara umum dan bagaimana komponen-komponennya berinteraksi dengan yang lain.

Berikut adalah diagram proses penguraian video:

Pertama, file media dimuat ke komponen yang disebut AVFormatContext (wadah video juga format). Bahkan, itu tidak sepenuhnya mengunduh seluruh file: seringkali hanya header yang dibaca.

Setelah mengunduh header minimum dari wadah kami , Anda dapat mengakses stream-nya (mereka dapat direpresentasikan sebagai data audio dan video dasar). Setiap aliran akan tersedia dalam komponen AVStream .

Misalkan video kami memiliki dua aliran: audio dikodekan menggunakan codec AAC , dan video dikodekan menggunakan H264 codec ( AVC ). Dari setiap aliran kita dapat mengekstraksi bagian data yang disebut paketyang dimuat ke dalam komponen yang disebut AVPacket .

Data di dalam paket masih dikodekan (dikompresi), dan untuk memecahkan kode paket kita perlu meneruskannya ke AVCodec tertentu .

AVCodec menerjemahkannya menjadi AVFrame , sebagai akibatnya komponen ini memberi kita bingkai yang tidak terkompresi. Perhatikan bahwa terminologi dan prosesnya sama untuk streaming audio dan video.

Persyaratan ↑


Karena terkadang ada masalah saat kompilasi atau menjalankan contoh, kami akan menggunakan Docker sebagai lingkungan pengembangan / runtime. Kami juga akan menggunakan video dengan kelinci besar , jadi jika Anda tidak memilikinya di komputer lokal Anda, jalankan saja perintah make fetch_small_bunny_video di konsol .

Sebenarnya, kode ↑


TLDR tunjukkan saya contoh kode yang dapat dieksekusi, bro:

$ make run_hello

Kami akan menghilangkan beberapa detail, tetapi jangan khawatir: kode sumber tersedia di github.

Kami akan mengalokasikan memori untuk komponen AVFormatContext , yang akan berisi informasi tentang format (wadah).

AVFormatContext *pFormatContext = avformat_alloc_context();

Sekarang kita akan membuka file, membaca header-nya dan mengisi AVFormatContext dengan informasi format minimal (perhatikan bahwa codec biasanya tidak terbuka). Untuk melakukan ini, gunakan fungsi avformat_open_input . Itu mengharapkan AVFormatContext , nama file, dan dua argumen opsional: AVInputFormat (jika Anda melewati NULL, FFmpeg akan menentukan format) dan AVDictionary (yang merupakan opsi demultiplexer).

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

Anda juga dapat mencetak nama format dan durasi media:

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

Untuk mengakses stream, kita perlu membaca data dari media. Ini dilakukan oleh fungsi avformat_find_stream_info . Sekarang pFormatContext-> nb_streams akan berisi jumlah benang, dan pFormatContext-> sungai [i] akan memberi kita i th aliran berturut-turut ( AVStream ).

avformat_find_stream_info(pFormatContext,  NULL);

Mari kita melalui loop di semua utas:

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

Untuk setiap aliran, kita akan menyimpan AVCodecParameters , yang menjelaskan properti dari codec yang digunakan oleh aliran ke- i :

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


Dengan menggunakan properti codec, kita dapat menemukan yang sesuai dengan meminta fungsi avcodec_find_decoder , kita juga dapat menemukan decoder terdaftar untuk pengenal codec dan mengembalikan AVCodec , komponen yang tahu cara menyandikan dan mendekodekan aliran:

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

Sekarang kita dapat mencetak informasi 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);

Dengan menggunakan codec, kami mengalokasikan memori untuk AVCodecContext , yang akan berisi konteks untuk proses decoding / encoding kami. Tapi kemudian Anda perlu mengisi konteks codec ini dengan parameter CODEC - kami melakukan ini menggunakan avcodec_parameters_to_context .

Setelah kami mengisi konteks codec, Anda perlu membuka codec. Kami memanggil fungsi avcodec_open2 dan kemudian kami dapat menggunakannya:

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

Sekarang kita akan membaca paket dari stream dan mendekode-kannya menjadi bingkai, tetapi pertama-tama kita perlu mengalokasikan memori untuk kedua komponen ( AVPacket dan AVFrame ).

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

Mari kita beri makan paket-paket kami dari aliran fungsi av_read_frame sementara ia memiliki paket-paket:

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

Sekarang kita akan mengirim paket data mentah (frame terkompresi) ke decoder melalui konteks codec menggunakan fungsi avcodec_send_packet :

avcodec_send_packet(pCodecContext, pPacket);

Dan mari kita dapatkan bingkai data mentah (frame yang tidak terkompresi) dari dekoder melalui konteks codec yang sama menggunakan fungsi avcodec_receive_frame :

avcodec_receive_frame(pCodecContext, pFrame);

Kami dapat mencetak nomor bingkai, PTS, DTS, jenis bingkai, dll:

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
);

Dan akhirnya, kita dapat menyimpan bingkai yang sudah diterjemahkan menjadi gambar abu-abu sederhana. Prosesnya sangat sederhana: kita akan menggunakan data pFrame-> , di mana indeks dikaitkan dengan ruang warna Y , Cb dan Cr . Cukup pilih 0 (Y) untuk menyimpan gambar abu-abu kami:

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);
}

Dan voila! Sekarang kami memiliki gambar grayscale 2MB:


Bab 1 - Sinkronisasi Audio dan Video ↑

Berada di dalam permainan adalah ketika pengembang muda JS menulis pemutar video MSE baru.
Sebelum kita mulai menulis kode transkode, mari kita bicara tentang sinkronisasi atau bagaimana pemutar video menemukan waktu yang tepat untuk memainkan bingkai.

Dalam contoh sebelumnya, kami menyimpan beberapa bingkai:


Saat kami mendesain pemutar video, kami harus memainkan setiap frame pada kecepatan tertentu, jika tidak, sulit untuk menikmati video karena diputar terlalu cepat atau terlalu lambat.

Oleh karena itu, kita perlu mendefinisikan beberapa logika untuk kelancaran pemutaran setiap frame. Dalam hal ini, setiap frame memiliki tanda representasi waktu ( PTS - dari p resent t t ime s tamp), yang merupakan peningkatan jumlah yang diperhitungkan dalam variabeltimebase , yang merupakan bilangan rasional (di mana penyebut dikenal sebagai skala waktu - skala waktu ) dibagi dengan frame rate ( fps ).

Lebih mudah dipahami dengan contoh. Mari kita simulasikan beberapa skenario.

Untuk fps = 60/1 dan timebase = 1/60000, setiap PTS akan meningkatkan skala waktu / fps = 1000 , sehingga waktu PTS nyata untuk setiap frame dapat (asalkan dimulai pada 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

Skenario yang hampir sama, tetapi dengan skala waktu sama dengan 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

Untuk fps = 25/1 dan timebase = 1/75, setiap PTS akan menambah skala waktu / fps = 3 , dan waktu PTS dapat:

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

Sekarang dengan pts_time kita dapat menemukan cara untuk memvisualisasikan ini dalam sinkronisasi dengan suara pts_time atau dengan jam sistem. FFmpeg libav menyediakan informasi ini melalui API-nya:

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


Karena penasaran, frame yang kami simpan dikirim dalam urutan DTS (frame: 1, 6, 4, 2, 3, 5), tetapi direproduksi dalam urutan PTS (frame: 1, 2, 3, 4, 5). Perhatikan juga berapa banyak bingkai B yang lebih murah dibandingkan dengan bingkai P atau 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]

Bab 2 - Remultiplexing ↑


Remultiplexing (penataan ulang, remuxing) - transisi dari satu format (wadah) ke yang lain. Misalnya, kita dapat dengan mudah mengganti video MPEG-4 dengan MPEG-TS menggunakan FFmpeg:

ffmpeg input.mp4 -c copy output.ts

File MP4 akan didemultipleks, sementara file tidak akan di-decode atau di-encode ( -c copy ), dan, pada akhirnya, kita mendapatkan file mpegts. Jika Anda tidak menentukan format -f , ffmpeg akan mencoba menebaknya berdasarkan ekstensi file.

Penggunaan umum FFmpeg atau libav mengikuti pola / arsitektur atau alur kerja seperti itu:

  • tingkat protokol - menerima data input (misalnya, file, tetapi juga bisa berupa unduhan rtmp atau HTTP)
  • β€” , , ,
  • β€”
  • β€” (, ),
  • … :
  • β€” ( )
  • β€” ( ) ( )
  • β€” , , ( , , )


(Grafik ini sangat terinspirasi oleh karya Leixiaohua dan Slhck )

Sekarang mari kita membuat contoh menggunakan libav untuk memberikan efek yang sama seperti ketika menjalankan perintah ini:

ffmpeg input.mp4 -c copy output.ts

Kita akan membaca dari input ( input_format_context ) dan mengubahnya ke output lain ( output_format_context ):

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

Biasanya, kita mulai dengan mengalokasikan memori dan membuka format input. Untuk kasus khusus ini, kita akan membuka file input dan mengalokasikan memori untuk file output:

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;
}

Kami akan remultiplex hanya streaming video, audio dan subtitle. Oleh karena itu, kami memperbaiki aliran mana yang akan kami gunakan dalam array indeks:

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

Segera setelah kami mengalokasikan memori yang diperlukan, kami perlu siklus melalui semua aliran, dan untuk masing-masing kita perlu membuat aliran output baru dalam konteks kita dari format output menggunakan fungsi avformat_new_stream . Harap perhatikan bahwa kami menandai semua aliran yang bukan video, audio, atau subtitle sehingga kami dapat mengabaikannya.

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;
  }
}

Sekarang buat file output:

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;
}

Setelah itu, Anda dapat menyalin stream, paket per paket, dari input kami ke stream output kami. Ini terjadi dalam satu lingkaran, selama ada paket ( av_read_frame ), untuk setiap paket Anda perlu menghitung ulang PTS dan DTS untuk akhirnya menuliskannya ( av_interleaved_write_frame ) ke dalam konteks format output kami.

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);
}

Untuk menyelesaikannya, kita perlu menulis stream stream ke file media keluaran menggunakan fungsi av_write_trailer :

av_write_trailer(output_format_context);

Sekarang kita siap untuk menguji kodenya. Dan tes pertama adalah konversi format (wadah video) dari MP4 ke file video MPEG-TS. Pada dasarnya kita membuat baris perintah ffmpeg input.mp4 -c untuk menyalin output.ts menggunakan libav.

make run_remuxing_ts

Berhasil! Jangan percaya padaku ?! Periksa dengan 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

Untuk meringkas apa yang telah kita lakukan, kita sekarang dapat kembali ke ide awal kita tentang bagaimana libav bekerja. Tetapi kami melewatkan bagian dari codec, yang ditampilkan dalam diagram.


Sebelum kita menyelesaikan bab ini, saya ingin menunjukkan bagian penting dari proses remultiplexing, di mana Anda dapat mengirimkan parameter ke multiplexer. Misalkan Anda ingin memberikan format MPEG-DASH, jadi Anda perlu menggunakan mp4 terfragmentasi (kadang-kadang disebut fmp4 ) alih-alih MPEG-TS atau MPEG-4 biasa.

Menggunakan baris perintah itu mudah:

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

Ini hampir sesederhana ini di versi libav, kami hanya melewatkan opsi saat menulis header output, segera sebelum menyalin paket:

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

Sekarang kita dapat menghasilkan file mp4 terfragmentasi ini:

make run_remuxing_fragmented_mp4

Untuk memastikan semuanya adil, Anda dapat menggunakan situs alat gpac / mp4box.js yang menakjubkan atau http://mp4parser.com/ untuk melihat perbedaannya - unduh mp4 pertama.

Seperti yang Anda lihat, ia memiliki satu blok mdat yang tidak dapat dibagi - ini adalah tempat di mana frame video dan audio berada. Sekarang unduh mp4 terfragmentasi untuk melihat bagaimana ia memperluas blok mdat:

Bab 3 - Transkode ↑


TLDR tunjukkan kode dan eksekusi:

$ make run_transcoding

Kami akan melewatkan beberapa detail, tetapi jangan khawatir: kode sumber tersedia di github.

Dalam bab ini, kita akan membuat transcoder minimalis yang ditulis dalam C, yang dapat mengkonversi video dari H264 ke H265 menggunakan libav librari FFmpeg, khususnya libavcodec , libavformat dan libavutil .


AVFormatContext adalah abstraksi untuk format file media, mis. untuk sebuah wadah (MKV, MP4, Webm, TS)
AVStream mewakili setiap tipe data untuk format yang diberikan (misalnya: audio, video, subtitle, metadata)
AVPacket adalah fragmen data terkompresi yang diterima dari AVStream yang dapat didekodekan menggunakan AVCodec (misalnya : av1, h264, vp9, hevc) menghasilkan data mentah yang disebut AVFrame .

Transmultiplexing ↑


Mari kita mulai dengan konversi sederhana, lalu muat file input.

// 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);

Sekarang konfigurasikan decoder. AVFormatContext akan memberi kita akses ke semua komponen AVStream , dan untuk masing-masing komponen kita bisa mendapatkan AVCodec mereka dan membuat AVCodecContext tertentu . Dan akhirnya, kita dapat membuka codec ini untuk menuju ke proses decoding.

AVCodecContext berisi data konfigurasi media, seperti kecepatan data, kecepatan bingkai, laju sampel, saluran, pitch, dan banyak lainnya.

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);
}

Anda juga perlu menyiapkan file media keluaran untuk konversi. Pertama, alokasikan memori untuk output AVFormatContext . Buat setiap aliran dalam format output. Untuk mengemas aliran dengan benar, salin parameter codec dari dekoder.

Atur flag AV_CODEC_FLAG_GLOBAL_HEADER , yang memberi tahu pembuat kode bahwa dia dapat menggunakan header global, dan akhirnya buka file output untuk menulis dan menyimpan header:

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);

Kami mendapatkan AVPacket dari decoder, menyesuaikan cap waktu dan menulis paket dengan benar ke file output. Terlepas dari kenyataan bahwa fungsi av_interleaved_write_frame melaporkan " write frame ", kami menyimpan paket tersebut. Kami menyelesaikan proses permutasi dengan menulis trailer aliran ke file.

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);

Transcoding ↑


Di bagian sebelumnya, ada program sederhana untuk konversi, sekarang kita akan menambahkan kemampuan untuk menyandikan file, khususnya, transcoding video dari h264 ke h265 .

Setelah decoder disiapkan, tetapi sebelum mengatur file media keluaran, konfigurasikan enkoder.

  • Buat AVStream video dalam encoder avformat_new_stream .
  • Kami menggunakan AVCodec dengan nama libx265 , avcodec_find_encoder_by_name .
  • Buat AVCodecContext berdasarkan pada codec avcodec_alloc_context3 yang dibuat .
  • Tetapkan atribut dasar untuk sesi transkode dan ...
  • ... buka codec dan salin parameter dari konteks ke aliran ( avcodec_open2 dan 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);

Diperlukan untuk memperluas siklus decoding untuk transcoding aliran video:

  • Kami mengirim AVPacket kosong ke decoder ( avcodec_send_packet ).
  • Dapatkan AVFrame yang tidak terkompresi ( avcodec_receive_frame ).
  • Kami mulai mengkode ulang bingkai mentah.
  • Kami mengirim bingkai mentah ( avcodec_send_frame ).
  • Kami mendapatkan kompresi berdasarkan codec AVPacket kami ( avcodec_receive_packet ).
  • Tetapkan stempel waktu ( av_packet_rescale_ts ).
  • Kami menulis ke file output ( 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;
}

Kami mengonversi aliran media dari h264 ke h265 . Seperti yang diharapkan, versi file media h265 lebih kecil dari h264, sementara program ini memiliki banyak peluang:

  /*
   * 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";

Dengan sepenuh hati, saya akui itu sedikit lebih rumit daripada yang tampak di awal. Saya harus memilih kode sumber baris perintah FFmpeg dan banyak menguji. Mungkin saya melewatkan sesuatu di suatu tempat, karena saya harus menggunakan force-cfr untuk h264 , dan beberapa pesan peringatan masih muncul, misalnya, bahwa jenis bingkai (5) secara paksa diubah ke jenis bingkai (3).

Terjemahan di Blog Edison:


All Articles