Я пытаюсь редактировать ширину и высоту 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 перед переходом к разговору на стороне сервера, у которого в последнее время было много проблем
Я понимаю. Что ж, если номера W/H в видеоконтейнере (MP4) кажутся неправильными, дважды проверьте их по сравнению с номерами в SPS самих видеоданных, чтобы быть уверенным. SPS обычно сохраняет истинный размер изображения. PS: Что касается проблем с W/H, вам придется задать конкретный вопрос... Некоторые файлы MP4 могут иметь несколько видеодорожек. Иногда значения в tkhd
могут быть установлены так, что некоторые значения ввода используют 8 байтов вместо 4 байтов. Это будет верно, если запись version
не является 0
. Некоторые MP4 имеют внутри кодек VP9 (и этот кодек может менять разрешение в разных кадрах)
PS: Записанный пример видео (VP9) меняющего разрешение во время воспроизведения. Надеюсь, ни один из ваших проблемных файлов MP4 не делает этого.
Прежде всего - спасибо, приятель. У большинства неудачных мп4 нет SPS, он должен быть внутри moov->trak[0]->mdia->minf->stbi->stsd
?
(1) Да -->minf-->stbl-->stsd
— это правильный путь для SPS, если он предусмотрен. (2) «Неудавшийся MP4 не имеет SPS» вместо 67
видите ли вы какое-либо значение, оканчивающееся на 7
(например: 27
или 47
)? Если нет, то во-вторых проверьте, существует ли SPS вокруг начальной области раздела mdat
. PS: Может это возможность сделать функцию восстановления мп4? Записав недостающие байты SPS и PPS (если их можно извлечь из части mdat
).. (3) Проверьте свой неисправный MP4 с помощью MediaInfo онлайн. Что написано под видео codecID
?
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...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:
Какую проблему вы пытаетесь решить, выполняя масштабирование пикселей путем редактирования байтов MP4? Если мы знаем, возможно, существует лучшее решение для достижения того же ожидаемого результата.