LINUX.ORG.RU

ffmpeg и ключевые кадры

 ,


0

2

Какая разница между -skip_frame nokey и -discard nokey?

В мануале пример для вырезания ключевых кадров из видео использует -skip_frame. Cоветы, которые мне попадались в сети, — с -discard, поэтому я раньше пользовался им. Хотя судя по числу результатов в Гугле, -skip_frame популярнее.

Из-за чего возник вопрос. Вырезал ключевые кадры из 17-секундного видео, насоздавало 16 картинок для первых 3 секунд и 4 для остального. Полез смотреть в avidemux — он увидел всего 6 ключевых кадров: на 0, 3, 6, 9, 12 и 15 секунд. Но команды

ffmpeg -discard nokey -i арба.mp4 -vsync 0 -f image2 'frames-%04d.png'
ffmpeg -discard nokey -i арба.mp4 -codec libwebp -vsync 0 'frames-%04d.webp'

помимо требуемых 6 также сохраняют 14 кадров между 0 и 3 секундами. А команды

ffmpeg -skip_frame nokey -i арба.mp4 -vsync 0 -f image2 'frames-%04d.png'
ffmpeg -skip_frame nokey -i арба.mp4 -codec libwebp -vsync 0 'frames-%04d.webp'

создают ожидаемые 6 файлов.

Почему так происходит?

★★★★★
Ответ на: комментарий от ox55ff

Я вычитал, что skip_frame отбрасывает кадры до декодирования, а discard после.

Погрепал по диагонали исходники.

  • -skip_frame - передается кодеку AVCodecContext как параметр skip_frame
  • -discard - передается потоку AVStream как параметр discard

Как маркируются фреймы при передаче между кодеком и потоком зависит от кодека. Также поддержка skip_frame зависит от кодека.

В общем, зависит от конкретной ситуации.

anonymous
()

Если кратко, то с ключевыми кадрами в h264 имеется каша, их есть несколько разных видов (вроде три я помню, может больше) и тебе повезёт, если ffmpeg их определяет именно так как ты хотел.

firkax ★★★★★
()
Ответ на: комментарий от firkax

с ключевыми кадрами в h264 имеется каша, их есть несколько разных видов (вроде три я помню

«reference», «bidirectional» и «key»? Эти 3 вида ffmpeg различает, и если фильтровать их, получится гораздо больше 20 кадров.

question4 ★★★★★
() автор топика
Ответ на: комментарий от ox55ff

Я вычитал, что skip_frame отбрасывает кадры до декодирования, а discard после. Наверное skip_frame популярнее, потому что меньше жрёт процессора.

Звучит логично, спасибо.

question4 ★★★★★
() автор топика
Ответ на: комментарий от question4

Не эксперт.

Сперва работает кодек и может без (полного) декодирования отбрасывать фреймы (skip_frame), если умеет. Отдает как поток декодированных фреймов, которые могут помечены разными типами, какие поддерживает кодек и как захочет их пометить, которые после можно фильтровать по типу (discard).

anonymous
()
Ответ на: комментарий от question4

Нет. Во-первых, давай определим что такое ключевой кадр: ключевой это тот, с которого можно начать проигрывание, отрезав (т.е. в них нельзя смотреть) все байты видеопотока, что были до него.

Есть три типа кадров вообще:

1) I-frame - этот кадр можно нарисовать не парся его соседей

2) P-frame - чтобы нарисовать этот кадр, надо перед этим нарисовать один или несколько более ранних (а те, в свою очередь могут требовать ещё какие-то)

3) B-frame - чтобы нарисовать его, надо нарисовать какие-то другие, как более ранние так и более поздние. На самом деле это сводится к p-frame: ссылаться всё равно можно только на кадры, которые уже распарсены, просто кадры в файле расположены немного не в том порядке как они будут проигрываться, то есть b-frame это зависимый кадр, перед которым расположена информация о более поздних чем он. Например: 1 2 4 3 5 тут третий кадр может зависеть от четвёртого, т.е. плеер распарсит четвёртый, запомнит его, потом распарсит и нарисует третий, потом нарисует четвёртый.

Никакой, кроме I-frame, очевидно ключевым быть не может. Но быть I-frame недостаточно чтобы быть ключевым.

Есть два вида i-кадров: non-IDR (это просто i-кадр как описано выше) и IDR. IDR означает, что никакой из более поздних кадров не зависит ни от какого из более ранних. Пример:

1 I
2 P (зависит от 1)
3 I
4 P (зависит от 2)
5 I
7 P (зависит от 5)
6 B (зависит от 5 и 7)
8 P (зависит от 7 и 6)
Вот тут третий кадр - non-IDR, потому что более позний кадр 4 зависит от более раннего 2. Пятый кадр - IDR, ни один из кадров 6,7,8 не зависит ни от одного из 1-4. Первый тоже IDR, потому что перед ним никого нет. IDR-кадр - очевидно ключевой, с какой стороны ни посмотри. О том, что кадр IDR, явно указано в закодированных h264-данных: для IDR-кадра предусмотрен отдельный тип пакета (блока данных в последовательности), а для всех остальных (обычные I,P,B) - другой, тут вопросов нет.

Однако, может попасться и такая ситуация: кадр не помечен как IDR, он просто I, но по факту ссылок из «потом» в «до» всё равно нет. Такой кадр тоже ключевой, но чтобы это узнать, надо например распарсить все кадры ролика, чтобы удостовериться что ссылок действительно нет.

Ну и наконец ещё одна штука: в стандарте h264 имеется среди прочего тип пакета (блока данных) «Supplemental Enchancement Information» (вспомогательная расширяющая информация, звучит как будто что-то не обязательное), и в этом пакете может быть опция «recovery point» (точка восстановления), навешиваемая на кадр, идущий следом за этим пакетом. Так вот эта опция означает «можно начать проигрывать с этого кадра» или другими словами «плеер, считай этот кадр ключевым». Причём у этой опции есть ещё параметр exact_match. Если включён ещё и он, то это «плеер, считай этот кадр ключевым, ты точно ничего не потеряешь если будешь так считать». Эти SEI recovery point, очевидно, предназначены для маркировки I-кадров. Чем отличается I-кадр в IDR-пакете от простого I-кадра, помеченного SEI recovery+exact_match, я не понял, по-моему это дублирование функционала. SEI recovery без exact match означает, что при старте проигрывания отсюда может быть немного артефактов поначалу, но в целом картинка удовлетворительная.

Итого, есть четыре варианта того, что ffmpeg считает ключевыми кадрами:

1) IDR I-frame

2) non-IDR I-frame + SEI recovery exact_match=1

3) non-IDR I-frame + SEI recovery exact_match=0

4) эвристически обнаруженные никак не помеченные ключевые кадры

Надёжные из них только первые два. Четвёртый скорее всего много где не примется, но в ffmpeg такое есть. А ещё h264-плеер в хромобразуерах не считает как минимум третий (а может и второй, не помню) тип за ключевой и ломается если ему дать видеопоток с него начинающийся (файрфокс работает норм).

Вот я делал патч (делал для ffmpeg 6.0 который был в фрибсд-портах 2023-06-12 но скорее всего подойдёт для многих разных версий) чтобы отключить распознавание спорных key-frames:

--- libavcodec/h264_parser.c.b  2024-04-01 00:18:13.000000000 +0300
+++ libavcodec/h264_parser.c    2024-04-01 00:17:34.000000000 +0300
@@ -357,10 +357,11 @@
             get_ue_golomb_long(&nal.gb);  // skip first_mb_in_slice
             slice_type   = get_ue_golomb_31(&nal.gb);
             s->pict_type = ff_h264_golomb_to_pict_type[slice_type % 5];
-            if (p->sei.recovery_point.recovery_frame_cnt >= 0) {
-                /* key frame, since recovery_frame_cnt is set */
-                s->key_frame = 1;
-            }
+// google chrome treats such frames as non-key and breaks dash stream
+//            if (p->sei.recovery_point.recovery_frame_cnt >= 0) {
+//                /* key frame, since recovery_frame_cnt is set */
+//                s->key_frame = 1;
+//            }
             pps_id = get_ue_golomb(&nal.gb);
             if (pps_id >= MAX_PPS_COUNT) {
                 av_log(avctx, AV_LOG_ERROR,
@@ -384,8 +385,9 @@
             sps       = p->ps.sps;

             // heuristic to detect non marked keyframes
-            if (p->ps.sps->ref_frame_count <= 1 && p->ps.pps->ref_count[0] <= 1 && s->pict_type == AV_PICTURE_TYPE_I)
-                s->key_frame = 1;
+// prevent spurious keyframes, as they break dash streams
+//            if (p->ps.sps->ref_frame_count <= 1 && p->ps.pps->ref_count[0] <= 1 && s->pict_type == AV_PICTURE_TYPE_I)
+//                s->key_frame = 1;

             p->poc.frame_num = get_bits(&nal.gb, sps->log2_max_frame_num);
Речь про h264 кодек, в других будет что-то своё, возможно похожее.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от firkax

Итого, есть четыре варианта того, что ffmpeg считает ключевыми кадрами:

Какой программой можно посмотреть свойства каждого кадра? Хотя бы для первых трёх типов, не заморачиваясь со сложными зависимостями.

question4 ★★★★★
() автор топика
Ответ на: комментарий от question4

Где посмотреть в нормальном виде - не знаю, я кастомный парсер писал чтобы эти SEI сдампить (но тоже некрасиво и полу вручную).

Вытащить h264 поток из mp4:

ffmpeg -i filename.mp4 -bsf:v h264_mp4toannexb -c:v copy -an filename.h264

Сдампить список всех кадров (с помощью grep отфильтруем только I т.к. остальные точно ни при чём) - на вход можно подавать как mp4 так и h264 но нам надо второй

ffprobe -loglevel error -select_streams v:0 -show_frames -show_entries frame=pts_time,coded_picture_number,key_frame,pict_type,pkt_pos,pkt_size -of csv <filename> | grep I
Что выводится:
frame,{is_key_frame},{pts_time},{pkt_pos},{pkt_size},{pict_type},{pict_num},{side_data_comment}

is_key_frame            0|1 - флаг того что ffmpeg считает это ключевым фреймом
pts_time                время
pkt_pos, pkt_size       смещение и длина x264-пакета в байтах от начала файла
pict_type               I|P|B - тип фрейма (I - фрейм самодостаточный, P - фрейм использует ссылки на предыдущие, B - фрейм использует ссылки в обе стороны)
pict_num                номер кадра в порядке записи их в файле (часто отличается от хронологического, в котором сортирует вывод ffprobe)
side_data_comment       тут может быть надпись про SEI-пакет
Пример вывода для mp4
frame,1,0.000000,35997,149,I,0,side_data,H.26[45] User Data Unregistered SEI message
frame,0,0.417083,42878,8038,I,10
frame,1,3.294958,298257,11158,I,79,side_data,H.26[45] User Data Unregistered SEI message
frame,1,5.755750,378112,3852,I,138
frame,1,7.340667,429019,5954,I,176,side_data,H.26[45] User Data Unregistered SEI message
frame,1,8.842167,558124,15076,I,212
frame,1,10.343667,782552,14262,I,248
frame,1,12.721042,950824,6719,I,305
frame,1,13.680333,1030238,6493,I,328,side_data,H.26[45] User Data Unregistered SEI message
frame,0,13.888875,1041652,9379,I,333
frame,0,14.097417,1064127,6763,I,338
frame,0,14.472792,1087293,5535,I,347
frame,0,14.597917,1095429,6231,I,350
Для h264
frame,1,N/A,0,187,I,0,side_data,H.26[45] User Data Unregistered SEI message
frame,0,N/A,359,8038,I,10
frame,1,N/A,209539,11196,I,79,side_data,H.26[45] User Data Unregistered SEI message
frame,1,N/A,250002,3851,I,138
frame,1,N/A,275689,5992,I,176,side_data,H.26[45] User Data Unregistered SEI message
frame,1,N/A,380575,15075,I,212
frame,1,N/A,581328,14261,I,248
frame,1,N/A,711348,6718,I,305
frame,1,N/A,775488,6531,I,328,side_data,H.26[45] User Data Unregistered SEI message
frame,0,N/A,783587,9379,I,333
frame,0,N/A,802808,6763,I,338
frame,0,N/A,820102,5535,I,347
frame,0,N/A,826257,6231,I,350
Время он теперь не знает, остальное на месте, изменились номера байтов в файле, что нам и надо. Там где ноль во второй колонке - ffmpeg считает кадр не ключевым, то есть он точно не IDR, можно забить. Там где единица - это либо IDR, либо SEI-recovery-point. Но отличить их по этой таблице мы не можем, см дальше.

Четвёртая колонка - смещение пакета от начала файла h264 Делаем так:

dd if=filename.h264 bs=1 count=16 skip=209539 | hexdump -C
dd if=filename.h264 bs=1 count=16 skip=250002 | hexdump -C
Видим:
00000000  00 00 00 01 06 05 2e dc  45 e9 bd e6 d9 48 b7 96  |........E....H..|
00000000  00 00 00 01 06 06 01 c4  80 00 00 01 41 88 9d 81  |............A...|
SEI-recovery-point выглядит так:
[00] 00 00 01 {06|26|46|66} 06 {length} {data}
Тут видно что первое (209539) - не recovery point а какой-то другой SEI, а вот второе (250002) как раз оно. Соответствует кадру номер 138 на 5.755750 секунде.

Первый нулевой байт необязателен (но тут он есть), length обычно равно 1, data - битовые флаги формата (слева направо, big endian):

ue(v) recovery_frame_cnt
u(1) exact_match_flag
u(1) broken_link_flag
u(2) changing_slice_group_idc

формат для ue(v):
Bit string     codeNum
  1            0
0 1 0          1
0 1 1          2
0 0 1 0 0      3
0 0 1 0 1      4
0 0 1 1 0      5
0 0 1 1 1      6
0 0 0 1 0 0 0  7
0 0 0 1 0 0 1  8
0 0 0 1 0 1 0  9
Соответственно, байт 0xC4 это 1 1 0 0 0 1 0 0

recovery_frame_cnt по таблице = 0

exact_match_flag = 1

broken_link_flag = 0

changing_slice_group_idc = 0 0

(я не особо вникал что значат все эти поля, но возможно тут открываются ещё разновидности key-frames)

дальше идёт запонитель битов 1 0 0 - его игнорируем. Итого, это кадр SEI recovery exact_match=1.

Однако, подозреваю что есть вероятность, что ffprobe выдаст смещение не SEI-пакета, а какого-то другого, расположенного раньше но связанного с тем же кадром. Тогда все манипуяции выше его не найдут, надо писать парсер. Вообще, тут тоже несложно: все h264-пакеты начинаются с последовательности байт 00,00,01 (и только они, случайно она встретиться не может), осталось их всех найти и проверить несколько первых их байт на предмет соответствия шаблону, потом сопоставить их смещения с тем что выдал ffprobe.

Ещё может быть ситуация когда кадр IDR, но на него всё равно навешано SEI recovery - чтобы его отличить опять надо сканировать все пакеты. IDR-кадр начинается с [00] 00 00 01 {05|25|45|65}.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 3)

интересно топикстартер получил ответ на свои вопросы; ffprobe -show_frames -print_format csv file.mp4|file.h264 должно быть достаточно

anonymous
()