FFprobe не отражает изменения размеров MP4

Я пытаюсь редактировать ширину и высоту MP4 без масштабирования.

Я делаю это, редактируя поля tkhd и stsd заголовка MP4.

  • exiftool покажет новую ширину и высоту, а ffprobe — нет.

Перед редактированием:

Exif:
$ exiftool $f | egrep -i 'width|height'

Image Width                     : 100
Image Height                    : 100
Source Image Width              : 100
Source Image Height             : 100

FFprobe:
$ ffprobe -v quiet -show_streams $f | egrep 'width|height'

width=100
height=100
coded_width=100
coded_height=100

После редактирования вышеуказанных размеров я получаю следующий вывод файла Python:

[ftyp] size:32
[mdat] size:196933
[moov] size:2057
- [mvhd] size:108
- [trak] size:1941
- - [tkhd] size:92
     Updated tkhd box: Width: 100 -> 300, Height: 100 -> 400
- - [mdia] size:1841
- - - [mdhd] size:32
- - - [hdlr] size:44
- - - [minf] size:1757
- - - - [vmhd] size:20
- - - - [dinf] size:36
- - - - - [dref] size:28
- - - - [stbl] size:1693
- - - - - [stsd] size:145
           Updated stsd box #1: Width: 100 -> 300, Height: 100 -> 400
- - - - - [stts] size:512
- - - - - [stss] size:56
- - - - - [stsc] size:28
- - - - - [stsz] size:924
- - - - - [stco] size:20

Затем снова запустите EXIFtool и FFprobe:

$ exiftool $f egrep -i 'width|height'

Image Width                     : 300
Image Height                    : 400
Source Image Width              : 300
Source Image Height             : 400

$ ffprobe -v quiet -show_streams $f | egrep 'width|height'

width=100
height=100
coded_width=100
coded_height=100

Это мой код Python:

import sys, struct

def read_box(f):
    offset = f.tell()
    header = f.read(8)
    if len(header) < 8:
        return None, offset
    size, box_type = struct.unpack(">I4s", header)
    box_type = box_type.decode("ascii")
    if size == 1:
        size = struct.unpack(">Q", f.read(8))[0]
    elif size == 0:
        size = None
    return {"type": box_type, "size": size, "start_offset": offset}, offset

def edit_tkhd_box(f, box_start, new_width, new_height, depth):
    f.seek(box_start + 84, 0)  # Go to the width/height part in tkhd box
    try:
        old_width = struct.unpack('>I', f.read(4))[0] >> 16
        old_height = struct.unpack('>I', f.read(4))[0] >> 16
        f.seek(box_start + 84, 0)  # Go back to write
        f.write(struct.pack('>I', new_width << 16))
        f.write(struct.pack('>I', new_height << 16))
        print(f"{'  ' * depth} Updated tkhd box: Width: {old_width} -> {new_width}, Height: {old_height} -> {new_height}")
    except struct.error:
        print(f"  Error reading or writing width/height to tkhd box")

def edit_stsd_box(f, box_start, new_width, new_height, depth):
    f.seek(box_start + 12, 0)  # Skip to the entry count in stsd box
    try:
        entry_count = struct.unpack('>I', f.read(4))[0]
        for i in range(entry_count):
            entry_start = f.tell()
            f.seek(entry_start + 4, 0)  # Skip the entry size
            format_type = f.read(4).decode("ascii", "ignore")
            if format_type == "avc1":
                f.seek(entry_start + 32, 0)  # Adjust this based on format specifics
                try:
                    old_width = struct.unpack('>H', f.read(2))[0]
                    old_height = struct.unpack('>H', f.read(2))[0]
                    f.seek(entry_start + 32, 0)  # Go back to write
                    f.write(struct.pack('>H', new_width))
                    f.write(struct.pack('>H', new_height))
                    print(f"{'  ' * depth} Updated stsd box #{i + 1}: Width: {old_width} -> {new_width}, Height: {old_height} -> {new_height}")
                except struct.error:
                    print(f"  Error reading or writing dimensions to avc1 format in entry {i + 1}")
            else:
                f.seek(entry_start + 8, 0)  # Skip to the next entry
    except struct.error:
        print(f"  Error reading or writing entries in stsd box")

def parse_and_edit_boxes(f, new_width, new_height, depth=0, parent_size=None):
    while True:
        current_pos = f.tell()
        if parent_size is not None and current_pos >= parent_size:
            break
        box, box_start = read_box(f)
        if not box:
            break
        box_type, box_size = box["type"], box["size"]
        print(f'{"- " * depth}[{box_type}] size:{box_size}')
        
        if box_type == "tkhd":
            edit_tkhd_box(f, box_start, new_width, new_height, depth)
        elif box_type == "stsd":
            edit_stsd_box(f, box_start, new_width, new_height, depth)
        
        # Recursively parse children if it's a container box
        if box_type in ["moov", "trak", "mdia", "minf", "stbl", "dinf", "edts"]:
            parse_and_edit_boxes(f, new_width, new_height, depth + 1, box_start + box_size)
        
        if box_size is None:
            f.seek(0, 2)  # Move to the end of file
        else:
            f.seek(box_start + box_size, 0)

if __name__ == '__main__':
    if len(sys.argv) != 4:
        print("Usage: python script.py <input_file> <new_width> <new_height>")
    else:
        with open(sys.argv[1], 'r+b') as f:
            parse_and_edit_boxes(f, int(sys.argv[2]), int(sys.argv[3]))

Кажется, это связано с ff_h264_decode_seq_parameter_set

Какую проблему вы пытаетесь решить, выполняя масштабирование пикселей путем редактирования байтов MP4? Если мы знаем, возможно, существует лучшее решение для достижения того же ожидаемого результата.

VC.One 27.07.2024 14:38

обнаружение (и лучшее понимание) ширины/высоты mp4 перед переходом к разговору на стороне сервера, у которого в последнее время было много проблем

James W. 27.07.2024 15:00

Я понимаю. Что ж, если номера W/H в видеоконтейнере (MP4) кажутся неправильными, дважды проверьте их по сравнению с номерами в SPS самих видеоданных, чтобы быть уверенным. SPS обычно сохраняет истинный размер изображения. PS: Что касается проблем с W/H, вам придется задать конкретный вопрос... Некоторые файлы MP4 могут иметь несколько видеодорожек. Иногда значения в tkhd могут быть установлены так, что некоторые значения ввода используют 8 байтов вместо 4 байтов. Это будет верно, если запись version не является 0. Некоторые MP4 имеют внутри кодек VP9 (и этот кодек может менять разрешение в разных кадрах)

VC.One 27.07.2024 15:59

PS: Записанный пример видео (VP9) меняющего разрешение во время воспроизведения. Надеюсь, ни один из ваших проблемных файлов MP4 не делает этого.

VC.One 27.07.2024 16:10

Прежде всего - спасибо, приятель. У большинства неудачных мп4 нет SPS, он должен быть внутри moov->trak[0]->mdia->minf->stbi->stsd?

James W. 27.07.2024 21:31

(1) Да -->minf-->stbl-->stsd — это правильный путь для SPS, если он предусмотрен. (2) «Неудавшийся MP4 не имеет SPS» вместо 67 видите ли вы какое-либо значение, оканчивающееся на 7 (например: 27 или 47)? Если нет, то во-вторых проверьте, существует ли SPS вокруг начальной области раздела mdat. PS: Может это возможность сделать функцию восстановления мп4? Записав недостающие байты SPS и PPS (если их можно извлечь из части mdat).. (3) Проверьте свой неисправный MP4 с помощью MediaInfo онлайн. Что написано под видео codecID?

VC.One 29.07.2024 15:53
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
6
76
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

FFprobe анализирует на уровне потока (например: H.264), но вы редактируете на уровне контейнера (например: MP4).

Вам потребуется отредактировать байты SPS (настройки параметров последовательности).
В частности, вы будете редактировать: pic_width_in_mbs_minus1 и pic_height_in_map_units_minus1.

Дважды проверьте следующее с помощью шестнадцатеричного редактора. Сначала попробуйте отредактировать вручную, а затем напишите код, чтобы добиться того же результата редактирования.

Вам также необходимо изучить как работают коды (числа) Голомба и Exp-Голомба. Потому что информация, которую нужно редактировать, хранится в том же битовом формате.

  • Байты SPS можно найти в поле avcC, которое находится внутри поля stsd MP4.

  • avcC имеет следующие значения (шестнадцатеричные цифры): 61 76 63 43.

  • Продолжайте идти вперед по байту, пока не нажмете FF (или 255), за которым следует E1 (или 225).

  • Теперь начинается SPS... два байта длины, затем сами байты SPS
    (начинается с байта 67, что означает «данные SPS»).

  • Прочтите эту запись в блоге (китайский) для получения дополнительной информации.
    Примечание. Если вы используете браузер Chrome, вы можете получить автоматический перевод страниц с китайского на английский.

Структура SPS показана на изображениях ниже.

Например, если ваши байты выглядят так: FF E1 00 19 67 42 C0 0D 9E 21 82 83 то...

  • После FF E1 начинается пакет SPS.
  • 00 19 — длина SPS в байтах (шестнадцатеричный 0x0019 равен десятичному 25).
  • Значение байта 0x67 сигнализирует о том, что здесь начинаются фактические данные SPS...
  • Профиль IDC 0x42 здесь имеет десятичное значение 66.

Вы можете видеть (на изображении ниже), что Profile IDC использует 8 бит, и поскольку слот массива содержит 8 бит, это значение будет значением всего слота.

Далее идет C0, который представляет собой четыре однобитовых значения и четыре зарезервированных нуля. Всего 8 бит, поэтому следующий слот массива заполняется как C0 (где биты C0 выглядят так: 1100 0000).

constraint_set0_flag = 1
constraint setl_flag = 1
constraint_set2_flag = 0
constraint_set3 flag = 0
reserved_zero_4bits  = 0 0 0 0

Далее идет 0D — уровень IDC.

Далее идет 9E, то есть биты 1001 1110. В формате ue(v), если первый бит равен 1, то ответ == 0 (например: мы останавливаемся каждый раз, когда обнаруживается бит 1, тогда ответом является то, сколько битов 0 было подсчитано до достижения этого бита 1)

seq_parameter_set_id = 0 (since first bit is a 1, we counted zero 0-bits to reach)

Здесь оператор IF можно пропустить, поскольку наш профиль IDC равен 66 (а не 100 и более).

В этом байте 0x9E осталось ещё 7 бит как ...001 1110

log2 max pic order cnt Isb minus4 = 3

Поскольку мы останавливаемся на любом следующем 1, мы используем количество предыдущих нулей для чтения битовой длины значения данных. Итак, здесь 001 11 следует читать как: 00 {1} 11 где это {1} является сигналом stop counting. Есть два нуля (перед 1), поэтому мы знаем, что после сигнала 1 нужно читать два бита для остановки)

Надеюсь, этого достаточно, чтобы начать работу вас и других читателей. Вы должны достичь pic_width_in_mbs_minus1.

Изображения структуры данных SPS:

SPS image #1SPS image #2

Другие вопросы по теме