本文主要记录一下成功使用pyav还原h264和265视频裸流字节流到图像的方法。
因为研究这个方法掉了不少头发。确实很麻烦,经过反复尝试、对比、测试、研究,再反复尝试、对比得到的结果。
经过n天的查找与尝试,分别测试opencv-python、ffmpeg-python和pyav,其实这三种都有很多不足与问题,但是没有办法,可用可选择的就这么点。
Opencv-python
其实最好用的是opencv-python也就是cv2,但是他无法传入字节流数据,只能传入摄像头索引index、url和磁盘路径。
FFmpeg-python
测试了下问题很多,兼容性很差,经常出现各种莫名其妙的bug。
PyAv
后来测试pyav,虽然pyav是套着ffmpeg,但是他具有将字节流转换成图像的方法,参考该网址
https://github.com/PyAV-Org/PyAV/blob/main/examples/basics/parse.py
虽然他具备该功能,但是实际测试中问题也是很多,研究了n天才摸索出来。
源数据
源数据是存储在磁盘中的海康摄像头裸流文件都是ps流,我需要一遍加载读取、一遍交给线程转换成图像显示在前台UI界面中。
方法1:使用av.open创建对象
经过测试,该方法可以,但是每次都需要创建新的av.open对象,并且会出现跳帧问题,也就是说当前正在播放第一帧图像,然后你二次byte_stream.write(video_bytes)后,按理说他应该播放第n帧,结果他会有解码出第1帧图像和第n帧,交叉显示。
f=open("c.h264","rb")
byte_stream = io.BytesIO()
def get_video_stream_bytes(f):
while chunk := f.read_bytes(10240):
yield chunk
for video_bytes in get_video_stream_bytes(f):
byte_stream.write(video_bytes)
byte_stream.seek(0)
container = av.open(byte_stream)
for frame in container.decode(video=0):
frame.to_image().save("/home/hi/a/%d.jpg"%i)
i+=1
print(i)
方法2:创建codec对象
该方法其实是我想要的,但是会出现一个问题,第一需要前期先创建对应解码器,因为视频流可能为265或264,需要做一次判断才可以。
其次最重要的是,如果出现高码流的视频,解码图像后会出现半幅绿屏。
半幅绿屏是我放弃该方法的原因。
fh = open("c.h264", "rb")
codec = av.CodecContext.create("h264", "r")
while True:
chunk = fh.read(1 << 16)
packets = codec.parse(chunk)
for packet in packets:
frames = codec.decode(packet)
for frame in frames:
frame.to_image().save("./tmp/%d.jpg"%i)
print("cc")
方法3:最终方案
该方法主要参考方法1进行改变,提供一个rawData对象,不断的清空、seek到0,但是这个方法需要注意,每次读取的块大小要大一些,否则ffmpeg会报很多错误。
其实可以根据nal头进行分帧导入,但是没有必要。直接分块读取就行了,简单粗暴。
该方法测试可行!
unsuported_byte_offset=True
这个非常重要,如果不设置为True,则视频帧会交叉显示第一帧与n帧
rawData.truncate(0)
这个也非常重要,必须要清空内存空间,否则也会交叉显示第一帧与n帧
import av
f=open("c.h264",'rb')
rawData = io.BytesIO()
container=None
video=None
#每次读取的块大小,建议5M以上
block_size=1024*1024*5
while True:
buffer=f.read(block_size)
#必须要清空
rawData.truncate(0)
rawData.write(buffer)
rawData.seek(0)
if container is None:
container = av.open(rawData)
video=container.streams.video[0]
video.thread_type='NONE'
else:
#unsuported_byte_offset=True 这个非常重要,如果不设置为True,则视频帧会交叉显示第一帧与n帧
container.seek(0,unsupported_byte_offset=True)
for frame in container.decode(video):
if frame.key_frame:
frame.to_image().save("./tmp/%d.jpg"%i)
i+=1