Безграничные возможности FFmpeg на примерах

dtadprv2ztglpevibimpf3o3wuu.png


FFmpeg — швейцарский нож для мультимедиа. Совершенно незаменимая программа, которую использует в повседневной жизни почти каждый, даже не зная об этом. Например, вы сняли видео и заливаете на видеохостинг — оно перекодируется и публикуется уже в другом разрешении/формате/размере. Как вы думаете, какая программа выполнила транскодирование? Вполне возможно, что под капотом на сервере работает FFmpeg.

FFmpeg объединяет более 300 видео/аудио/графических кодеков, декордеров, муксеров, демуксеров и фильтров. Благодаря ему вы можете написать собственный видеоплеер в тысячу строчек кода, не разбираясь с кишочками видеообработки.

Это «движок» почти всех современных инструментов для обработки/сжатия/редактирования видео. Они просто предлагают графический интерфейс с кнопками, а ffmpeg делает реальную работу.
Другими словами, существует основной движок FFmpeg, а весь остальной софт — это прокладки/GUI/плееры/редакторы, которые дают к нему доступ. Но можно обойтись без посредников и использовать FFmpeg напрямую. Только в консоли. Последняя версия программы лежит здесь (вместе с сопровождающими инструментами FFplay и FFprobe).

Для примера, вот несколько практических задач, с которыми приходится сталкиваться в повседневной рутине. Все они решаются однострочниками за несколько секунд.

▍ Вырезать кусочек видео, сохранить его в GIF


Стандартная ситуация, когда хочется вырезать из фильма или видеотрансляции какой-то жест или эмоцию и отправить в мессенджер как анимацию. Например, в Телеграме анимации можно отправлять в форматах GIF и MP4.

FFmpeg вырезает видеофрагмент и конвертирует его в GIF следующей командой:

ffmpeg -i input.mp4 -an -ss '00:11:02.5' -to '00:11:05' output.gif


В команде указан фреймрейт gif (10), время начала и конца видео. Как вариант, можно указать время начала и продолжительность фрагмента.

Примечание. Для генерации простых команд ffmpeg в первое время можно пользоваться «помощником» ffmpeg buddy.

4hytzcabwec8ahkawp3m4uzg6aw.png


▍ Пережать другим кодеком


Например, мы оцифровали старую VHS-кассету в программе HandBrake (подключили с видеомагнитофона по USB), не применяли никаких фильтров и сохранили MPEG2 разрешением 720×576. С трёхчасовой кассеты получился файл размером 6,4 ГБ.

Понятно, что кассете 30 лет, качество сигнала плохое, тем более это вторая или третья копия, которая была ужасна в момент своего появления. Но выбирать не приходится.

ddxov-yrd38n4v8vzactnzypv-c.png


Глядя на исходный MPEG2, возникают вопросы:

  1. Как сжать это видео (с потерями качества), чтобы быстро закинуть одноклассникам в Телеграм? Качество такое, что снижение разрешения не играет особой роли.
  2. Как сохранить это видео для длительного хранения? Существуют ли видеокодеки для сжатия без потерь, как в случае с фотографиями?


С первым вопросом всё довольно просто. Указываем в FFmpeg масштабирование 50% по горизонтали и вертикали, кодируем стандартным кодеком:

ffmpeg -i input.mpg -vf 'scale=iw*0.5:ih*0.5' output.mp4


Здесь масштабирование указано относительно оригинала.

Или так:

ffmpeg -i input.mpg -vf 'scale=360:288' output.mp4


Здесь масштабирование указано в пикселях (половина от оригинального 720×576). Результат будет идентичный:

Результат
ffmpeg -i input.mpg -vf 'scale=360:288' output.mp4
ffmpeg version 5.0.1-essentials_build-www.gyan.dev Copyright (c) 2000-2022 the FFmpeg developers
  built with gcc 11.2.0 (Rev7, Built by MSYS2 project)
  configuration: --enable-gpl --enable-version3 --enable-static --disable-w32threads --disable-autodetect --enable-fontconfig --enable-iconv --enable-gnutls --enable-libxml2 --enable-gmp --enable-lzma --enable-zlib --enable-libsrt --enable-libssh --enable-libzmq --enable-avisynth --enable-sdl2 --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxvid --enable-libaom --enable-libopenjpeg --enable-libvpx --enable-libass --enable-libfreetype --enable-libfribidi --enable-libvidstab --enable-libvmaf --enable-libzimg --enable-amf --enable-cuda-llvm --enable-cuvid --enable-ffnvcodec --enable-nvdec --enable-nvenc --enable-d3d11va --enable-dxva2 --enable-libmfx --enable-libgme --enable-libopenmpt --enable-libopencore-amrwb --enable-libmp3lame --enable-libtheora --enable-libvo-amrwbenc --enable-libgsm --enable-libopencore-amrnb --enable-libopus --enable-libspeex --enable-libvorbis --enable-librubberband
  libavutil      57. 17.100 / 57. 17.100
  libavcodec     59. 18.100 / 59. 18.100
  libavformat    59. 16.100 / 59. 16.100
  libavdevice    59.  4.100 / 59.  4.100
  libavfilter     8. 24.100 /  8. 24.100
  libswscale      6.  4.100 /  6.  4.100
  libswresample   4.  3.100 /  4.  3.100
  libpostproc    56.  3.100 / 56.  3.100
Input #0, mpeg, from 'input.mpg':
  Duration: 02:26:12.00, start: 0.193178, bitrate: 6296 kb/s
  Stream #0:0[0x1e0]: Video: mpeg2video (Main), yuv420p(tv, bt470bg, top first), 720x576 [SAR 16:15 DAR 4:3], 25 fps, 25 tbr, 90k tbn
    Side data:
      cpb: bitrate max/min/avg: 8500000/0/0 buffer size: 1835008 vbv_delay: N/A
  Stream #0:1[0x1c0]: Audio: mp2, 48000 Hz, stereo, s16p, 224 kb/s
File 'output.mp4' already exists. Overwrite? [y/N] n
Not overwriting - exiting
PS D:\> .\ffmpeg -i input.mpg -vf 'scale=360:288' output.mp4
ffmpeg version 5.0.1-essentials_build-www.gyan.dev Copyright (c) 2000-2022 the FFmpeg developers
  built with gcc 11.2.0 (Rev7, Built by MSYS2 project)
  configuration: --enable-gpl --enable-version3 --enable-static --disable-w32threads --disable-autodetect --enable-fontconfig --enable-iconv --enable-gnutls --enable-libxml2 --enable-gmp --enable-lzma --enable-zlib --enable-libsrt --enable-libssh --enable-libzmq --enable-avisynth --enable-sdl2 --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxvid --enable-libaom --enable-libopenjpeg --enable-libvpx --enable-libass --enable-libfreetype --enable-libfribidi --enable-libvidstab --enable-libvmaf --enable-libzimg --enable-amf --enable-cuda-llvm --enable-cuvid --enable-ffnvcodec --enable-nvdec --enable-nvenc --enable-d3d11va --enable-dxva2 --enable-libmfx --enable-libgme --enable-libopenmpt --enable-libopencore-amrwb --enable-libmp3lame --enable-libtheora --enable-libvo-amrwbenc --enable-libgsm --enable-libopencore-amrnb --enable-libopus --enable-libspeex --enable-libvorbis --enable-librubberband
  libavutil      57. 17.100 / 57. 17.100
  libavcodec     59. 18.100 / 59. 18.100
  libavformat    59. 16.100 / 59. 16.100
  libavdevice    59.  4.100 / 59.  4.100
  libavfilter     8. 24.100 /  8. 24.100
  libswscale      6.  4.100 /  6.  4.100
  libswresample   4.  3.100 /  4.  3.100
  libpostproc    56.  3.100 / 56.  3.100
Input #0, mpeg, from 'input.mpg':
  Duration: 02:26:12.00, start: 0.193178, bitrate: 6296 kb/s
  Stream #0:0[0x1e0]: Video: mpeg2video (Main), yuv420p(tv, bt470bg, top first), 720x576 [SAR 16:15 DAR 4:3], 25 fps, 25 tbr, 90k tbn
    Side data:
      cpb: bitrate max/min/avg: 8500000/0/0 buffer size: 1835008 vbv_delay: N/A
  Stream #0:1[0x1c0]: Audio: mp2, 48000 Hz, stereo, s16p, 224 kb/s
Stream mapping:
  Stream #0:0 -> #0:0 (mpeg2video (native) -> h264 (libx264))
  Stream #0:1 -> #0:1 (mp2 (native) -> aac (native))
Press [q] to stop, [?] for help
[libx264 @ 00000202ddab6e80] using SAR=16/15
[libx264 @ 00000202ddab6e80] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[libx264 @ 00000202ddab6e80] profile High, level 2.1, 4:2:0, 8-bit
[libx264 @ 00000202ddab6e80] 264 - core 164 r3094 bfc87b7 - H.264/MPEG-4 AVC codec - Copyleft 2003-2022 - http://www.videolan.org/x264.html - options: cabac=1 ref=3 deblock=1:0:0 analyse=0x3:0x113 me=hex subme=7 psy=1 psy_rd=1.00:0.00 mixed_ref=1 me_range=16 chroma_me=1 trellis=1 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=-2 threads=6 lookahead_threads=1 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=25 scenecut=40 intra_refresh=0 rc_lookahead=40 rc=crf mbtree=1 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=1:1.00
Output #0, mp4, to 'output.mp4':
  Metadata:
    encoder         : Lavf59.16.100
  Stream #0:0: Video: h264 (avc1 / 0x31637661), yuv420p(tv, bt470bg, top coded first (swapped)), 360x288 [SAR 16:15 DAR 4:3], q=2-31, 25 fps, 12800 tbn
    Metadata:
      encoder         : Lavc59.18.100 libx264
    Side data:
      cpb: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: N/A
  Stream #0:1: Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 128 kb/s
    Metadata:
      encoder         : Lavc59.18.100 aac
frame=219300 fps=209 q=-1.0 Lsize=  711087kB time=02:26:12.01 bitrate= 664.1kbits/s speed=8.35x
video:566821kB audio:137780kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.920495%
[libx264 @ 00000202ddab6e80] frame I:1143  Avg QP:22.93  size: 10857
[libx264 @ 00000202ddab6e80] frame P:59982 Avg QP:25.64  size:  5013
[libx264 @ 00000202ddab6e80] frame B:158175 Avg QP:29.16  size:  1690
[libx264 @ 00000202ddab6e80] consecutive B-frames:  2.9%  1.9%  2.9% 92.3%
[libx264 @ 00000202ddab6e80] mb I  I16..4:  5.2% 71.9% 22.9%
[libx264 @ 00000202ddab6e80] mb P  I16..4:  2.2% 13.8%  3.0%  P16..4: 40.6% 26.7% 12.7%  0.0%  0.0%    skip: 1.1%
[libx264 @ 00000202ddab6e80] mb B  I16..4:  0.3%  1.1%  0.2%  B16..8: 43.7% 11.8%  2.6%  direct:14.1%  skip:26.3%  L0:38.2% L1:42.7% BI:19.1%
[libx264 @ 00000202ddab6e80] 8x8 transform intra:72.0% inter:73.2%
[libx264 @ 00000202ddab6e80] coded y,uvDC,uvAC intra: 75.8% 92.3% 56.3% inter: 31.9% 62.8% 4.8%
[libx264 @ 00000202ddab6e80] i16 v,h,dc,p: 30% 26%  3% 40%
[libx264 @ 00000202ddab6e80] i8 v,h,dc,ddl,ddr,vr,hd,vl,hu: 24% 22% 12%  5%  7%  9%  7%  8%  7%
[libx264 @ 00000202ddab6e80] i4 v,h,dc,ddl,ddr,vr,hd,vl,hu: 23% 26% 11%  5%  8%  9%  7%  7%  5%
[libx264 @ 00000202ddab6e80] i8c dc,h,v,p: 47% 21% 20% 11%
[libx264 @ 00000202ddab6e80] Weighted P-Frames: Y:31.1% UV:22.8%
[libx264 @ 00000202ddab6e80] ref P L0: 48.6% 19.1% 20.9%  9.5%  1.9%
[libx264 @ 00000202ddab6e80] ref B L0: 89.7%  8.4%  1.8%
[libx264 @ 00000202ddab6e80] ref B L1: 96.4%  3.6%
[libx264 @ 00000202ddab6e80] kb/s:529.34
[aac @ 00000202ddab8780] Qavg: 356.888


Потеря качества практически не заметна на экране телефона, а размер файла уменьшился до 694 МБ. Уже влезает в бесплатный лимит телеграма (и даже на CD, если кому-то отправить по почте).

Согласно документации, кодирование в H.265 займёт втрое больше времени, зато файл получится на 25–50% меньше, чем H.264. Но это пока относительно новый формат, который поддерживается не на всех старых устройствах. Даже FFmpeg по умолчанию его пока не использует. У меня не получилось закодировать в H.265 с настройками максимального качества и уменьшением разрешения в одной команде. Наверное, нужно сначала переконвертировать в lossless-формат, потом уменьшить до 50% кадра, а потом кодировать в H.265.

Кстати, перекодирование с уменьшением размера видео можно выполнить бесплатно на серверах Google, если залить оригиналы на YouTube, затем скачать результаты транскодинга программой yt-dlp и удалить исходники. Это удобно, если у вас сотни часов видеоматериала и не хочется нагружать свой ПК.

Эксперименты показывают, что любые кодеки с потерей качества заметно портят картинку, независимо от настроек. Казалось бы, надо найти вариант с перекодированием без потери качества (lossless). Теоретически, такие варианты есть. Ряд видеокодеков поддерживают режим сжатия без потерь, вот некоторые из них:

  • H.264 lossless,
  • H.265 lossless,
  • Motion JPEG 2000 lossless,
  • JPEG XS lossless,
  • FFV1,
  • AV1,
  • VP9,
  • Apple Animation (QuickTime RLE),
  • … и другие.


FFmpeg использует энкодеры x264/x265, а также нативно поддерживает свободный кодек без потерь FFV1, который по параметрам компрессии похож на Motion JPEG 2000 lossless, но за счёт более быстрых алгоритмов может кодировать в реальном времени.

▍ Извлечение аудиодорожки


Например, у нас большая видеозапись, а мы хотим её загрузить в плеер/смартфон для прослушивания, чтобы не записывать гигабайты сопутствующего видео. Нет ничего проще:

ffmpeg -i input.mpg -map 0:1 bbb_audio.mp3


Если не указаны дополнительные параметры, то всё кодируется с настройками по умолчанию для MP3.

Результат
ffmpeg -i input.mpg -map 0:1 bbb_audio.mp3
ffmpeg version 5.0.1-essentials_build-www.gyan.dev Copyright (c) 2000-2022 the FFmpeg developers
  built with gcc 11.2.0 (Rev7, Built by MSYS2 project)
  configuration: --enable-gpl --enable-version3 --enable-static --disable-w32threads --disable-autodetect --enable-fontconfig --enable-iconv --enable-gnutls --enable-libxml2 --enable-gmp --enable-lzma --enable-zlib --enable-libsrt --enable-libssh --enable-libzmq --enable-avisynth --enable-sdl2 --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxvid --enable-libaom --enable-libopenjpeg --enable-libvpx --enable-libass --enable-libfreetype --enable-libfribidi --enable-libvidstab --enable-libvmaf --enable-libzimg --enable-amf --enable-cuda-llvm --enable-cuvid --enable-ffnvcodec --enable-nvdec --enable-nvenc --enable-d3d11va --enable-dxva2 --enable-libmfx --enable-libgme --enable-libopenmpt --enable-libopencore-amrwb --enable-libmp3lame --enable-libtheora --enable-libvo-amrwbenc --enable-libgsm --enable-libopencore-amrnb --enable-libopus --enable-libspeex --enable-libvorbis --enable-librubberband
  libavutil      57. 17.100 / 57. 17.100
  libavcodec     59. 18.100 / 59. 18.100
  libavformat    59. 16.100 / 59. 16.100
  libavdevice    59.  4.100 / 59.  4.100
  libavfilter     8. 24.100 /  8. 24.100
  libswscale      6.  4.100 /  6.  4.100
  libswresample   4.  3.100 /  4.  3.100
  libpostproc    56.  3.100 / 56.  3.100
Input #0, mpeg, from 'input.mpg':
  Duration: 02:26:12.00, start: 0.193178, bitrate: 6296 kb/s
  Stream #0:0[0x1e0]: Video: mpeg2video (Main), yuv420p(tv, bt470bg, top first), 720x576 [SAR 16:15 DAR 4:3], 25 fps, 25 tbr, 90k tbn
    Side data:
      cpb: bitrate max/min/avg: 8500000/0/0 buffer size: 1835008 vbv_delay: N/A
  Stream #0:1[0x1c0]: Audio: mp2, 48000 Hz, stereo, s16p, 224 kb/s
Stream mapping:
  Stream #0:1 -> #0:0 (mp2 (native) -> mp3 (libmp3lame))
Press [q] to stop, [?] for help
Output #0, mp3, to 'bbb_audio.mp3':
  Metadata:
    TSSE            : Lavf59.16.100
  Stream #0:0: Audio: mp3, 48000 Hz, stereo, s16p
    Metadata:
      encoder         : Lavc59.18.100 libmp3lame
size=  137063kB time=02:26:12.00 bitrate= 128.0kbits/s speed=31.5x
video:0kB audio:137063kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.000169%


С параметром -ss можно вырезать нужный фрагмент аудиодорожки.

▍ Любая магия


Даже самый лаконичный и понятный мануал по FFmpeg занимает сотню страниц печатного текста. Про официальную документацию не стоит даже упоминать, там объём скорее в тысячу страниц.

Глядя на фантастические примеры использования FFmpeg, поражаешься мощи этого инструмента. Например, вот пользователь захотел поиграть в игры PS2 на своём ноутбуке. Но поскольку тот недостаточно мощный для эмулятора PCSX2, ему пришла идея транслировать видео с домашнего сервера в другом конце города. То есть он пробросил геймпад на рабочую станцию через USB/IP и начал копать мануал FFmpeg, как транслировать игру на ноутбук. FFmpeg захватывает видео напрямую из буферов KMS, использует аппаратное ускорение GPU для транскодирования со снижением разрешения, захватывает аудио, всё перекодирует с параметрами для передачи по сети (в реальном времени) и передаёт результат в UDP-сокет. На другом конце он настроил плеер mpv для получения и воспроизведения потока.

ffmpeg \
  -f pulse \
  -i alsa_output.platform-snd_aloop.0.analog-surround-51.monitor \
  -f kmsgrab \
  -thread_queue_size 64 \   # уменьшает задержку ввода
  -i - \
  # Capture and downscale frames on the GPU:
  -vf 'hwmap=derive_device=vaapi,scale_vaapi=1280:720,hwdownload,format=bgr0' \
  -c:v libx264 \
  -preset:v superfast \     # максимально быстрое кодирование видео
  -tune zerolatency \       # настройка кодека на нулевую задержку
  -intra-refresh 1 \        # уменьшает задержку и уменьшает потери пакетов
  -f mpegts \               # объединение в поток mpegts, который хорошо подходит для данного случая
  -b:v 3M \                 # целевая полоса пропускания для итогового видео
  udp://$hackerspace:41841


Всего час изучения руководства — и мы с нуля составляем рабочую команду FFmpeg, чтобы играть в PS2 на ноутбуке через стрим с удалённого сервера. Практически Google Stadia своими руками (земля ей пухом).Или вот пример из мануала:

ffmpeg -y  \
    -ss 20 -t 60 -i bbb_sunflower_1080p_60fps_normal.mp4 \
    -i train.jpg \
    -ss 4 -i voice_recording.wav \
    -filter_complex "[0:v]hue=h=80:s=1[main] ; [1:v]crop=w=382:h=304:x=289:y=227[train] ; [main][train]overlay=x=200:y=200,vignette=PI/4[video] ; [2:a]volume=1.5,aecho=0.8:0.9:100:0.3[speech] ; [0:a][speech]amix=duration=shortest,asplit[audio1][audio2]" \
    -map '[video]' -map '[audio1]' -metadata title="Editor's cut" bbb_edited.mp4 \
    -map '[audio2]' bbb_edited_audio_only.wav


Эта команда делает в том числе следующее:

  1. Открывает файл.
  2. Вырезает фрагмент (-ss 20 -t 60).
  3. Накладывает изображение train.jpg.
  4. Обрезает наложенное изображение (crop).
  5. Добавляет эффект виньетирования под углом PI/4.
  6. Накладывает фильтр для коррекции тона (hue).
  7. Накладывает дополнительную звуковую дорожку voice_recording.wav.
  8. Увеличивает громкость.
  9. Добавляет эхо.
  10. Экспортирует результат в разные форматы.
  11. Экспортирует оригинальную звуковую дорожку.


Одна команда в консоли заменяет десять минут редактирования в Premiere Pro!

Оригинал (видео, картинка поезда, звук) и результат (справа):

o-rspwbxwkelav4syskocslfqki.png


▍ Редактор LosslessCut как GUI для FFmpeg


Как мы уже говорили, многие программы для обработки видео «под капотом» используют FFmpeg. И похоже, что FFmpeg начинает осваивать веб-технологии. Например, недавно появился ffmpeg.wasm — чистый порт FFmpeg на WebAssembly/JavaScript. Это обработка видео нативно в браузере.

Ещё одна интересная новинка последнего времени — видеоредактор LosslessCut. Он вырезает и склеивает фрагменты видео практически мгновенно и без потери качества, не выполняя перекодирование, как Adobe Premiere Pro. Привычная получасовая операция тут занимает пять секунд, экономя при этом гигабайты дискового пространства.

tf1vy9r46kwi3t2pv-615yl3jhc.jpeg

LosslessCut

Конечно, такая магия работает только при условии, что фрагменты видео/аудио сжаты одинаковым кодеком. В данном случае не требуется повторная обработка и фрагменты достаточно просто склеить.

Такая функциональность лучше всего подходит для следующих задач:

  1. Быстрый монтаж небольших видеороликов.
  2. Вырезание/добавление фрагментов. Например, вот что можно сделать:
    • вырезать рекламные ролики из записанной телепередачи;
    • удалить аудио- или видеодорожку из файла;
    • добавить музыку в видео (или заменить существующую звуковую дорожку);
    • добавить субтитры (или произвольные надписи) в видеоряд;
    • выполнить слияние файлов (с идентичными параметрами кодирования);
    • извлечь любые/все дорожки из файла;
      rbdfwseqc44y8plx65occvpel5m.png

    • извлечь музыку, видео или субтитры из видео и обрезать их в соответствии с вашими потребностями;
    • повернуть видео, для которого установлен флаг неправильной ориентации, например, видео с телефона.

  3. Быстрое перекодирование H264 MKV в MOV или MP4 для воспроизведения на iPhone.
  4. Журнал последних команд ffmpeg, чтобы изменять и запускать эти команды из консоли.
  5. Зацикливание видео/аудио X раз без повторного кодирования.
  6. Экспорт/импорт вырезанных сегментов в CSV.


Примечание: доступность отдельных функций зависит от формата/кодека.

8wg0u12wqxiwcbxtet3pbafdd3u.png


В общем, очень полезный инструмент. Неудивительно, что под его капотом тоже работает FFmpeg. То есть LosslessCut — просто удобный и эффективный FFmpeg GUI. Разумеется, исходный код тоже открыт. Если есть желание задонатить автору, то программу можно купить через Mac App Store или Microsoft Store. Версия под Linux распространяется через Snap Store и Flathub (а также напрямую с GitHub).
FFmpeg выполняет практически любые операции с мультимедийными файлами, какие только можно представить. Остаётся упомянуть добрым словом гениального программиста Фабриса Беллара, который запустил этот опенсорсный проект в 2000 году и несколько первых лет возглавлял разработку. Человечеству нужен был такой инструмент. Теперь он у нас есть, проблема решена, а Фабрис работает над другими крайне важными задачами.

Telegram-канал с розыгрышами призов, новостями IT и постами о ретроиграх

sz7jpfj8i1pa6ocj-eia09dev4q.png

© Habrahabr.ru