HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>В этой статье мы рассмотрим, каким образом в<strong>Linux/FFmpeg</strong>можно организовать кодовую базу на языке C с учётом расширяемости, работающей, как будто в C есть<strong>полиморфизм</strong>. Также обсудим, каким образом концепция Linux "всё - файл" функционирует на уровне исходного кода. И почему FFmpeg даёт возможность легко и быстро осуществлять добавление поддержки новых кодеков и форматов.</p>
1 <p>В этой статье мы рассмотрим, каким образом в<strong>Linux/FFmpeg</strong>можно организовать кодовую базу на языке C с учётом расширяемости, работающей, как будто в C есть<strong>полиморфизм</strong>. Также обсудим, каким образом концепция Linux "всё - файл" функционирует на уровне исходного кода. И почему FFmpeg даёт возможность легко и быстро осуществлять добавление поддержки новых кодеков и форматов.</p>
2 <h2>Введение</h2>
2 <h2>Введение</h2>
3 <p>Не секрет, что хороший код, написанный качественно, окупится впоследствии, особенно если продукт усложнится. Чтобы такой код создать, программисты подбирают специальные шаблоны, объединяя их в<strong>абстракции</strong>. В нашем случае именно так и поступили разработчики Linux и FFmpeg. Речь идёт о том, что в процессе разработки ПО создаются структуры данных, причём определяется их поведение и зависимости. А то, каким образом они построены и взаимосвязаны, мы можем рассматривать как<strong>архитектуру ПО</strong>или, если хотите, дизайн.</p>
3 <p>Не секрет, что хороший код, написанный качественно, окупится впоследствии, особенно если продукт усложнится. Чтобы такой код создать, программисты подбирают специальные шаблоны, объединяя их в<strong>абстракции</strong>. В нашем случае именно так и поступили разработчики Linux и FFmpeg. Речь идёт о том, что в процессе разработки ПО создаются структуры данных, причём определяется их поведение и зависимости. А то, каким образом они построены и взаимосвязаны, мы можем рассматривать как<strong>архитектуру ПО</strong>или, если хотите, дизайн.</p>
4 <p>Допустим, мы разрабатываем фреймворк, необходимый для обработки аудио- и видеофайлов. Следует учесть, что кодеки H264, AV1, HEVC и AAC некоторые операции с данными выполняют<strong>идентично</strong>, поэтому, разработав<strong>обобщённую абстракцию</strong>, можно будет задействовать её вместо того, чтобы плотно работать с каждым отдельным кодеком.</p>
4 <p>Допустим, мы разрабатываем фреймворк, необходимый для обработки аудио- и видеофайлов. Следует учесть, что кодеки H264, AV1, HEVC и AAC некоторые операции с данными выполняют<strong>идентично</strong>, поэтому, разработав<strong>обобщённую абстракцию</strong>, можно будет задействовать её вместо того, чтобы плотно работать с каждым отдельным кодеком.</p>
5 <p>Другой неплохой приём - использование слабосвязанных компонентов, но для этого потребуется чётко определить их функции.</p>
5 <p>Другой неплохой приём - использование слабосвязанных компонентов, но для этого потребуется чётко определить их функции.</p>
6 <h2>Ruby</h2>
6 <h2>Ruby</h2>
7 <p>Концепцию гораздо проще понять на практическом примере. Давайте создадим (примерно) набросок фреймворка, необходимый для обработки потоковых данных с использованием нескольких различных кодеков:</p>
7 <p>Концепцию гораздо проще понять на практическом примере. Давайте создадим (примерно) набросок фреймворка, необходимый для обработки потоковых данных с использованием нескольких различных кодеков:</p>
8 class AV1 def encode(bytes) end def decode(bytes) end end class H264 def encode(bytes) end def decode(bytes) end end # ... supported_codecs = [AV1.new, H264.new, HEVC.new] class MediaFramework def encode(type, bytes) codec = supported_codecs.find {|c| c.class.name.downcase == type} codec.encode(bytes) end end<p>Без конкретизации в нашем коде мы предполагаем, что каждый кодек реализует такие функции, как encode и decode. А так как Ruby является языком с динамической типизацией, то любой из классов способен иметь реализацию данных 2-х операций, работая как кодек.</p>
8 class AV1 def encode(bytes) end def decode(bytes) end end class H264 def encode(bytes) end def decode(bytes) end end # ... supported_codecs = [AV1.new, H264.new, HEVC.new] class MediaFramework def encode(type, bytes) codec = supported_codecs.find {|c| c.class.name.downcase == type} codec.encode(bytes) end end<p>Без конкретизации в нашем коде мы предполагаем, что каждый кодек реализует такие функции, как encode и decode. А так как Ruby является языком с динамической типизацией, то любой из классов способен иметь реализацию данных 2-х операций, работая как кодек.</p>
9 <p>Этот дизайн можно назвать довольно неплохим, ведь, если надо будет добавить новый кодек, потребуется лишь включить в список его реализацию. Да, список мы можем сделать и динамическим. Но вообще, смысл примера заключается в том, что данный<strong>код легкорасширяем и хорошо поддерживается</strong>, поскольку компоненты между собой связаны слабо, а каждый из этих компонентов делает лишь то, что должен.</p>
9 <p>Этот дизайн можно назвать довольно неплохим, ведь, если надо будет добавить новый кодек, потребуется лишь включить в список его реализацию. Да, список мы можем сделать и динамическим. Но вообще, смысл примера заключается в том, что данный<strong>код легкорасширяем и хорошо поддерживается</strong>, поскольку компоненты между собой связаны слабо, а каждый из этих компонентов делает лишь то, что должен.</p>
10 <p>Кстати, к определённым способам организации кода подталкивает и фреймворк Ruby on Rails, но уже через архитектуру MVC.</p>
10 <p>Кстати, к определённым способам организации кода подталкивает и фреймворк Ruby on Rails, но уже через архитектуру MVC.</p>
11 <h2>Go</h2>
11 <h2>Go</h2>
12 <p>Когда мы обращаемся к языкам программирования со статической типизацией, мы должны быть более формальными, когда описываем требуемые типы. Тем не менее мы всё же можем создать код, который аналогичен вышеописанному примеру:</p>
12 <p>Когда мы обращаемся к языкам программирования со статической типизацией, мы должны быть более формальными, когда описываем требуемые типы. Тем не менее мы всё же можем создать код, который аналогичен вышеописанному примеру:</p>
13 type Codec interface { Encode(data []int) ([]int, error) Decode(data []int) ([]int, error) } type H264 struct { } func (H264) Encode(data []int) ([]int, error) { // Здесь много кода return data, nil } var supportedCodecs := []Codec{H264{}, AV1{}} func Encode(codec string, data int[]) { // Здесь возможно выбрать e, применяя // supportedCodecs[0].Encode(data) }<p>Итак, тип interface в Go существенно мощнее той же конструкции в Java, ведь его определение не связано с реализацией, впрочем, как и наоборот. Мы даже можем присвоить тип ReadWriter каждому кодеку и использовать его в таком виде.</p>
13 type Codec interface { Encode(data []int) ([]int, error) Decode(data []int) ([]int, error) } type H264 struct { } func (H264) Encode(data []int) ([]int, error) { // Здесь много кода return data, nil } var supportedCodecs := []Codec{H264{}, AV1{}} func Encode(codec string, data int[]) { // Здесь возможно выбрать e, применяя // supportedCodecs[0].Encode(data) }<p>Итак, тип interface в Go существенно мощнее той же конструкции в Java, ведь его определение не связано с реализацией, впрочем, как и наоборот. Мы даже можем присвоить тип ReadWriter каждому кодеку и использовать его в таком виде.</p>
14 <h2>С</h2>
14 <h2>С</h2>
15 <p>На "Си" мы также можем создать код со схожим поведением при некоторых отличиях:</p>
15 <p>На "Си" мы также можем создать код со схожим поведением при некоторых отличиях:</p>
16 struct Codec { *int (*encode)(*int); *int (*decode)(*int); }; *int h264_encode(int *bytes) { // ... } *int h264_decode(int *bytes) { // ... } struct Codec av1 = { .encode = av1_encode, .decode = av1_decode }; struct Codec h264 = { .encode = h264_encode, .decode = h264_decode }; int main(int argc, char *argv[]) { h264.encode(argv[1]); }<p>Смотрите, изначально в обобщённой структуре определяются абстрактные операции (в нашем случае функции). Потом они наполняются конкретным кодом, к примеру, декодером и кодером кодека AV1.</p>
16 struct Codec { *int (*encode)(*int); *int (*decode)(*int); }; *int h264_encode(int *bytes) { // ... } *int h264_decode(int *bytes) { // ... } struct Codec av1 = { .encode = av1_encode, .decode = av1_decode }; struct Codec h264 = { .encode = h264_encode, .decode = h264_decode }; int main(int argc, char *argv[]) { h264.encode(argv[1]); }<p>Смотрите, изначально в обобщённой структуре определяются абстрактные операции (в нашем случае функции). Потом они наполняются конкретным кодом, к примеру, декодером и кодером кодека AV1.</p>
17 <p>Многие другие языки поддерживают схожую логику распределения методов либо функций, как будто они придерживаются какой-либо конвенции.</p>
17 <p>Многие другие языки поддерживают схожую логику распределения методов либо функций, как будто они придерживаются какой-либо конвенции.</p>
18 <p>В итоге софту на уровне операционной системы достаточно лишь уметь работать с показанными абстракциями высокого уровня.</p>
18 <p>В итоге софту на уровне операционной системы достаточно лишь уметь работать с показанными абстракциями высокого уровня.</p>
19 <h2>Linux kernel и непосредственно концепция "всё - файл"</h2>
19 <h2>Linux kernel и непосредственно концепция "всё - файл"</h2>
20 <p>Эта концепция операционной системы "Линукс" даёт нам возможность применять один интерфейс при работе с любыми системными ресурсами. Допустим, при обработке сетевых сокетов, особых файлов и даже USB-устройств как файлов.</p>
20 <p>Эта концепция операционной системы "Линукс" даёт нам возможность применять один интерфейс при работе с любыми системными ресурсами. Допустим, при обработке сетевых сокетов, особых файлов и даже USB-устройств как файлов.</p>
21 <p>Такой подход упрощает программную разработку для ОС, ведь мы получаем возможность применять прекрасно исследованный набор операций для абстракции, которая названа "файлом".</p>
21 <p>Такой подход упрощает программную разработку для ОС, ведь мы получаем возможность применять прекрасно исследованный набор операций для абстракции, которая названа "файлом".</p>
22 <p>Посмотрим, как это функционирует:</p>
22 <p>Посмотрим, как это функционирует:</p>
23 # В 1-м, самом простом случае, мы просто читаем несложный текстовый файл $ cat /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ... # Тут мы действуем, как будто считываем простой файл, # но на самом деле всё не так (хотя технически, разумеется, именно так) $ cat /proc/meminfo MemTotal: 2046844 kB MemFree: 546984 kB MemAvailable: 1535688 kB Buffers: 162676 kB Cached: 892000 kB # В конце концов, мы открываем файл с помощью fd=3 для чтения/записи # Данный "файл" на самом деле представляет собой сокет # потом мы отправляем запрос этому файлу &gt;&amp;3 # и из него же считываем $ exec 3&lt;&gt; /dev/tcp/www.google.com/80 $ printf 'HEAD / HTTP/1.1\nHost: www.google.com\nConnection: close\n\n' &gt;&amp;3 $ cat &lt;&amp;3 HTTP/1.1 200 OK Date: Wed, 21 Aug 2019 12:48:40 GMT Expires: -1 Cache-Control: private, max-age=0 Content-Type: text/html; charset=ISO-8859-1 P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info." Server: gws X-XSS-Protection: 0 X-Frame-Options: SAMEORIGIN Set-Cookie: 1P_JAR=2019-08-21-12; expires=Fri, 20-Sep-2019 12:48:40 GMT; path=/; domain=.google.com Set-Cookie: NID=188=K69nLKjqge87Ymv4h-gAW_lRfLCo7-KrTf01ULtY278lUUcaNxlEqXExDtVB104pdA8CLUZI8LMvJv26P_D8RMF3qCDzLTpjji96B9v_miGlZOIBro6pDreHP0yW7dz-9myBfOgdQjroAc0wWvOAkBu-zgFW_Of9VpK3IfIaBok; expires=Thu, 20-Feb-2020 12:48:40 GMT; path=/; domain=.google.com; HttpOnly Accept-Ranges: none Vary: Accept-Encoding Connection: close<p>Всё это становится возможным лишь из-за того, что концепция разрабатывалась как один из основных способов подсистемного взаимодействия. Давайте посмотрим на участок API-структуры file_operations:</p>
23 # В 1-м, самом простом случае, мы просто читаем несложный текстовый файл $ cat /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ... # Тут мы действуем, как будто считываем простой файл, # но на самом деле всё не так (хотя технически, разумеется, именно так) $ cat /proc/meminfo MemTotal: 2046844 kB MemFree: 546984 kB MemAvailable: 1535688 kB Buffers: 162676 kB Cached: 892000 kB # В конце концов, мы открываем файл с помощью fd=3 для чтения/записи # Данный "файл" на самом деле представляет собой сокет # потом мы отправляем запрос этому файлу &gt;&amp;3 # и из него же считываем $ exec 3&lt;&gt; /dev/tcp/www.google.com/80 $ printf 'HEAD / HTTP/1.1\nHost: www.google.com\nConnection: close\n\n' &gt;&amp;3 $ cat &lt;&amp;3 HTTP/1.1 200 OK Date: Wed, 21 Aug 2019 12:48:40 GMT Expires: -1 Cache-Control: private, max-age=0 Content-Type: text/html; charset=ISO-8859-1 P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info." Server: gws X-XSS-Protection: 0 X-Frame-Options: SAMEORIGIN Set-Cookie: 1P_JAR=2019-08-21-12; expires=Fri, 20-Sep-2019 12:48:40 GMT; path=/; domain=.google.com Set-Cookie: NID=188=K69nLKjqge87Ymv4h-gAW_lRfLCo7-KrTf01ULtY278lUUcaNxlEqXExDtVB104pdA8CLUZI8LMvJv26P_D8RMF3qCDzLTpjji96B9v_miGlZOIBro6pDreHP0yW7dz-9myBfOgdQjroAc0wWvOAkBu-zgFW_Of9VpK3IfIaBok; expires=Thu, 20-Feb-2020 12:48:40 GMT; path=/; domain=.google.com; HttpOnly Accept-Ranges: none Vary: Accept-Encoding Connection: close<p>Всё это становится возможным лишь из-за того, что концепция разрабатывалась как один из основных способов подсистемного взаимодействия. Давайте посмотрим на участок API-структуры file_operations:</p>
24 struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); // ... }<p>Такая структура весьма чётко определяет то, что принято подразумевать под концепцией файла, а также то, какое именно поведение мы ожидаем:</p>
24 struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); // ... }<p>Такая структура весьма чётко определяет то, что принято подразумевать под концепцией файла, а также то, какое именно поведение мы ожидаем:</p>
25 const struct file_operations ext4_dir_operations = { .llseek = ext4_dir_llseek, .read = generic_read_dir, // ... };<p>Ниже можем посмотреть на набор функций, которые реализуют это поведение в файловой системе ext4:</p>
25 const struct file_operations ext4_dir_operations = { .llseek = ext4_dir_llseek, .read = generic_read_dir, // ... };<p>Ниже можем посмотреть на набор функций, которые реализуют это поведение в файловой системе ext4:</p>
26 static const struct file_operations proc_cpuinfo_operations = { .open = cpuinfo_open, .read = seq_read, .llseek = seq_lseek, .release = seq_release, };<p>Обратите внимание что даже cpuinfo proc-файлы реализованы посредством этой абстракции. То есть по факту при работе с файлами под "Линукс", вы применяете VFS, а она, в свою очередь, выполняет обращение к абстрактным функциям.</p>
26 static const struct file_operations proc_cpuinfo_operations = { .open = cpuinfo_open, .read = seq_read, .llseek = seq_lseek, .release = seq_release, };<p>Обратите внимание что даже cpuinfo proc-файлы реализованы посредством этой абстракции. То есть по факту при работе с файлами под "Линукс", вы применяете VFS, а она, в свою очередь, выполняет обращение к абстрактным функциям.</p>
27 <h2>FFmpeg - форматы</h2>
27 <h2>FFmpeg - форматы</h2>
28 <p>Теперь посмотрим на общую схему архитектуры FFmpeg-процессов, демонстрирующих, что внутренние компоненты, в основном, связаны посредством таких абстрактных концепций, как AVCodec. То есть они не связаны напрямую через конкретные кодеки.</p>
28 <p>Теперь посмотрим на общую схему архитектуры FFmpeg-процессов, демонстрирующих, что внутренние компоненты, в основном, связаны посредством таких абстрактных концепций, как AVCodec. То есть они не связаны напрямую через конкретные кодеки.</p>
29 <p>Идём дальше. Для входящих файлов в FFmpeg осуществляется создание структуры AVInputFormat, реализуемой посредством любого формата (видеоконтейнера), который нужно использовать. Что касается файлов MKV, то они тоже<a>заполняют эту структуру</a>собственной реализацией, своей реализацией заполняет структуру и<a>MP4-формат</a>.</p>
29 <p>Идём дальше. Для входящих файлов в FFmpeg осуществляется создание структуры AVInputFormat, реализуемой посредством любого формата (видеоконтейнера), который нужно использовать. Что касается файлов MKV, то они тоже<a>заполняют эту структуру</a>собственной реализацией, своей реализацией заполняет структуру и<a>MP4-формат</a>.</p>
30 typedef struct AVInputFormat { const char *name; const char *long_name; const char *extensions; const char *mime_type; ff_const59 struct AVInputFormat *next; int raw_codec_id; int priv_data_size; int (*read_probe)(const AVProbeData *); int (*read_header)(struct AVFormatContext *); } // matroska AVInputFormat ff_matroska_demuxer = { .name = "matroska,webm", .long_name = NULL_IF_CONFIG_SMALL("Matroska / WebM"), .extensions = "mkv,mk3d,mka,mks", .priv_data_size = sizeof(MatroskaDemuxContext), .read_probe = matroska_probe, .read_header = matroska_read_header, .read_packet = matroska_read_packet, .read_close = matroska_read_close, .read_seek = matroska_read_seek, .mime_type = "audio/webm,audio/x-matroska,video/webm,video/x-matroska" }; // mov (mp4) AVInputFormat ff_mov_demuxer = { .name = "mov,mp4,m4a,3gp,3g2,mj2", .long_name = NULL_IF_CONFIG_SMALL("QuickTime / MOV"), .priv_class = &amp;mov_class, .priv_data_size = sizeof(MOVContext), .extensions = "mov,mp4,m4a,3gp,3g2,mj2", .read_probe = mov_probe, .read_header = mov_read_header, .read_packet = mov_read_packet, .read_close = mov_read_close, .read_seek = mov_read_seek, .flags = AVFMT_NO_BYTE_SEEK | AVFMT_SEEK_TO_PTS, };<p>Данный дизайн даёт возможность осуществлять простую интеграцию новых кодеков, форматов и протоколов. А весной 2019 г. в FFmpeg включили DAV1d-кодек и, после<a>изучения изменений в коде</a>, вы увидите, насколько успешно прошло это внедрение. В итоге ему нужно лишь зарегаться в качестве доступного кодека, а потом придерживаться списка общих операций.</p>
30 typedef struct AVInputFormat { const char *name; const char *long_name; const char *extensions; const char *mime_type; ff_const59 struct AVInputFormat *next; int raw_codec_id; int priv_data_size; int (*read_probe)(const AVProbeData *); int (*read_header)(struct AVFormatContext *); } // matroska AVInputFormat ff_matroska_demuxer = { .name = "matroska,webm", .long_name = NULL_IF_CONFIG_SMALL("Matroska / WebM"), .extensions = "mkv,mk3d,mka,mks", .priv_data_size = sizeof(MatroskaDemuxContext), .read_probe = matroska_probe, .read_header = matroska_read_header, .read_packet = matroska_read_packet, .read_close = matroska_read_close, .read_seek = matroska_read_seek, .mime_type = "audio/webm,audio/x-matroska,video/webm,video/x-matroska" }; // mov (mp4) AVInputFormat ff_mov_demuxer = { .name = "mov,mp4,m4a,3gp,3g2,mj2", .long_name = NULL_IF_CONFIG_SMALL("QuickTime / MOV"), .priv_class = &amp;mov_class, .priv_data_size = sizeof(MOVContext), .extensions = "mov,mp4,m4a,3gp,3g2,mj2", .read_probe = mov_probe, .read_header = mov_read_header, .read_packet = mov_read_packet, .read_close = mov_read_close, .read_seek = mov_read_seek, .flags = AVFMT_NO_BYTE_SEEK | AVFMT_SEEK_TO_PTS, };<p>Данный дизайн даёт возможность осуществлять простую интеграцию новых кодеков, форматов и протоколов. А весной 2019 г. в FFmpeg включили DAV1d-кодек и, после<a>изучения изменений в коде</a>, вы увидите, насколько успешно прошло это внедрение. В итоге ему нужно лишь зарегаться в качестве доступного кодека, а потом придерживаться списка общих операций.</p>
31 +AVCodec ff_libdav1d_decoder = { + .name = "libdav1d", + .long_name = NULL_IF_CONFIG_SMALL("dav1d AV1 decoder by VideoLAN"), + .type = AVMEDIA_TYPE_VIDEO, + .id = AV_CODEC_ID_AV1, + .priv_data_size = sizeof(Libdav1dContext), + .init = libdav1d_init, + .close = libdav1d_close, + .flush = libdav1d_flush, + .receive_frame = libdav1d_receive_frame, + .capabilities = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_AUTO_THREADS, + .caps_internal = FF_CODEC_CAP_INIT_THREADSAFE | FF_CODEC_CAP_INIT_CLEANUP | + FF_CODEC_CAP_SETS_PKT_DTS, + .priv_class = &amp;libdav1d_class, + .wrapper_name = "libdav1d", +};`<p>Итак, вне зависимости от применяемого нами языка программирования мы всегда можем пробовать создавать слабозависимый код с повышенной согласованностью. Как раз эти 2 свойства и дают нам возможность писать софт, который потом будет легко расширять и так же легко поддерживать.</p>
31 +AVCodec ff_libdav1d_decoder = { + .name = "libdav1d", + .long_name = NULL_IF_CONFIG_SMALL("dav1d AV1 decoder by VideoLAN"), + .type = AVMEDIA_TYPE_VIDEO, + .id = AV_CODEC_ID_AV1, + .priv_data_size = sizeof(Libdav1dContext), + .init = libdav1d_init, + .close = libdav1d_close, + .flush = libdav1d_flush, + .receive_frame = libdav1d_receive_frame, + .capabilities = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_AUTO_THREADS, + .caps_internal = FF_CODEC_CAP_INIT_THREADSAFE | FF_CODEC_CAP_INIT_CLEANUP | + FF_CODEC_CAP_SETS_PKT_DTS, + .priv_class = &amp;libdav1d_class, + .wrapper_name = "libdav1d", +};`<p>Итак, вне зависимости от применяемого нами языка программирования мы всегда можем пробовать создавать слабозависимый код с повышенной согласованностью. Как раз эти 2 свойства и дают нам возможность писать софт, который потом будет легко расширять и так же легко поддерживать.</p>
32 <p>Источник -<a>"Good Code Design From Linux/Kernel"</a>.</p>
32 <p>Источник -<a>"Good Code Design From Linux/Kernel"</a>.</p>
33  
33