我正在尝试编辑 mp4 宽度和高度而不缩放
我通过编辑 tkhd 和 stsd 框来做到这一点
-
exiftool
将显示新的宽度和高度,但ffprobe
将 不会
之前:
$ exiftool $f | egrep -i 'width|height'
Image Width : 100
Image Height : 100
Source Image Width : 100
Source Image Height : 100
$ ffprobe -v error -select_streams v:0 -show_entries 'stream=width,height' -of 'csv=s=x:p=0' $f
100x100
以下 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 error -select_streams v:0 -show_entries 'stream=width,height' -of 'csv=s=x:p=0' $f
100x100
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]))
这个问题的根源在于你混淆了视频文件中存储的两种不同的“分辨率”:
-
存储分辨率:
这是视频实际编码的分辨率,也就是视频帧的像素尺寸。你通过修改
avc1
atom中的宽度和高度值来改变的就是这个分辨率。 -
显示分辨率:
这是视频播放时应该显示的分辨率,它不一定与存储分辨率相同。播放器会根据存储分辨率、显示比例、画面裁剪等信息来决定最终的显示分辨率。你修改
tkhd
atom中的宽度和高度值影响的是这个分辨率。
ffprobe默认显示的是存储分辨率,而exiftool显示的则是显示分辨率。
这就是为什么你修改了
avc1
atom后,exiftool能识别到变化,而ffprobe没有变化的原因。
为了让ffprobe也显示修改后的分辨率,你需要 重新编码视频 ,使存储分辨率和显示分辨率保持一致。你可以使用以下命令来重新编码视频:
ffmpeg -i input.mp4 -c:v libx264 -vf scale=300:400 output.mp4
这个命令会使用
libx264
编码器将视频重新编码为300x400的分辨率,并将结果保存到
output.mp4
文件中。
另外,还需要注意以下几点:
-
修改
tkhd
atom中的分辨率信息并不会改变视频的实际分辨率,只是改变了播放器对视频显示分辨率的理解。如果显示分辨率与存储分辨率不匹配,播放器可能会对视频进行拉伸或裁剪,导致画面变形。 - 直接修改视频文件可能会导致文件损坏,建议在修改之前备份原始文件。
- 如果你只想改变视频的显示比例而不改变实际分辨率,可以使用视频播放器提供的画面缩放功能。
希望这些信息能帮到你!
标签:python,mp4,ffprobe,exiftool From: 78795606