Я пытаюсь редактировать ширину и высоту 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
🤔 А знаете ли вы, что...
В Python есть множество библиотек и фреймворков для разработки веб-приложений.
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: