请选择 进入手机版 | 继续访问电脑版
MSIPO技术圈 首页 IT技术 查看内容

Pyav代码分析

2023-07-13

PyAV提供了ffmpeg的python接口,但实际它只是使用ffmpeg做后端,使用Cython封装了ffmpeg的接口,所以实际调用的还是ffmpeg。

也就是说,PyAV用类封装了ffmpeg提供的API,如果想要使用,关键还是要看懂其整体架构。
PYAV用类封装了ffmpeg的几个关键结构体

名称作用
packet封装了ffmpegde AVPacket
frame封装了ffmpeg的AVframe
stream封装了ffmpeg的AVStream
option封装了ffmpeg的AVOption
InputContainer封装了ffmpeg的avformat_open_input demux
OutputContainer封装了ffmpeg的av_interleaved_write_frame mux
CodecContext封装了ffmpeg codec相关代码

具体使用的,如果你有自己的ffmpeg,那么先编译安装自己的ffmpeg,然后:

pip install av --no-binary av

如果没有自己的ffmpeg:

pip install av

安装好之后就可以使用了。

下面先看几个简单的案例:

import os
import subprocess
import logging
import time

logging.basicConfig(level=logging.DEBUG)
logging.getLogger('libav').setLevel(logging.DEBUG)

import av
import av.datasets




# We want an H.264 stream in the Annex B byte-stream format.
# We haven't exposed bitstream filters yet, so we're gonna use the `ffmpeg` CLI.
h264_path = "libx264_640x360_baseline_5_frames.h264"
# if not os.path.exists(h264_path):
#     subprocess.check_call(
#         [
#             "ffmpeg",
#             "-i",
#             av.datasets.curated("zzsin_1920x1080_60fps_60s.mp4"),
#             "-vcodec",
#             "copy",
#             "-an",
#             "-bsf:v",
#             "h264_mp4toannexb",
#             h264_path,
#         ]
#     )




fh = open(h264_path, "rb")

codec = av.CodecContext.create("h264_efcodec", "r")
codec.options={"hw_id":"15"}
codec.open()
print(codec.name)
first= True
count=0
while True:
    chunk = fh.read(1 << 16)
    packets = codec.parse(chunk)
    print("Parsed {} packets from {} bytes:".format(len(packets), len(chunk)))

    for packet in packets:
        print("   ", packet)
        frames = codec.decode(packet)
        
        if first:
            time.sleep(2)
            first=False
        for frame in frames:
            print("       ", frame)
            count+=1
            print('--count:%d--'%count)
            frame.to_image().save("night-sky.{:04d}.jpg".format(count),quality=80,)

    # We wait until the end to bail so that the last empty `buf` flushes
    # the parser.
    if not chunk:
        break

p=av.Packet(None)
print("send eos:", p)
frames = codec.decode(p) 
for frame in frames:
    print("       ", frame)
    count+=1
    print('--count:%d--'%count)
    frame.to_image().save("night-sky.{:04d}.jpg".format(count),quality=80,)


print('all count:%d'%count)
codec.close()

上面是通过创建codec来进行解码的案例,可以在创建的时候指定解码器名称以及可以设置option。这里注意一点就是这里只能parse annexb格式的视频流 AVCC的视频流是不能在这里解析的。

下面是另外一个demux codec的案例:

import time

import av
import av.datasets

container = av.open('ocr_400_400_5_frames.mp4')
first=True

count=0
start_time = time.time()
for packet in container.demux():
    print(packet)
    for frame in packet.decode():
        print(frame)
        count+=1
        print('---frame:%d---'%count)
    if first:
        time.sleep(2)
        first=False

auto_time = time.time() - start_time
container.close()
print('all frame:%d',count)

这里的codec是container中内置的一个解码器,这里的解码器是无法自主选择具体使用那个解码器的。

综合上面两个案例,我们可以使用下面的方法来解码:

import os
import subprocess
import logging
import time

import av

logging.basicConfig(level=logging.DEBUG)
logging.getLogger('libav').setLevel(logging.DEBUG)

h264_path = "ocr_400_400_5_frames.mp4"
input_ = av.open(h264_path,options={"vsync":"0"})
in_stream = input_.streams.video[0]

codec = av.CodecContext.create("h264_efcodec", "r")
codec.options={"hw_id":"15"}
# codec.options={"device_id":"0"}
codec.open()
print(codec.name)
# print(codec.extradata_size)
codec.extradata =in_stream.codec_context.extradata

first=True
num = 0

for packet in input_.demux(in_stream):
    print('----packet---')
    packet.dts =0
    packet.pts = 0
    print("   ", packet)

    frames = codec.decode(packet)
    print('---after decode---')
    if first:
        time.sleep(2)
        first=False
    for frame in frames:
        print("       ", frame)
        num+=1
        print('-----frame:%d-----'%num)

print('all:%d'%num)
codec.close()

上面这个案例结合了第一个和第二个解码的使用方法,在这里我们采用demux+decode(自己设置的解码器)。不要觉得这里很简单,这是我看完整个封装源代码才搞清楚的,当然这里唯一的缺点是inpoutcontainer内部为了分配condec,多占用了一些内存,不过这也无所谓了。

看完上面实例,可能发现一个sleep(2),为什么要加这句?主要是因为,我们硬件解码器open()的时候花费的时间较长,这里增加sleep函数来等待底下硬件解码器完全启动,不然会出现所有的输入数据送完了,解码器一帧数据都还没有解码出来。这里又引出PyAV的一个局限,它只能调用封装后的decode()接口,无法调用更加细粒度的ffmpeg接口,导致无法像ffmpeg那样通过循环调用avcodec_receive_frame()来取解码后的数据。

static int decode(AVCodecContext *dec_ctx) {
   int ret;
    AVPacket packet;
    AVFrame *p_frame;
    int eos = 0;

    p_frame = av_frame_alloc();

    while(1) {
        ret = av_read_frame(g_ifmt_ctx, &packet);
        if (ret == AVERROR_EOF) {
            av_log(g_dec_ctx, AV_LOG_INFO, "av_read_frame got eof\n");
            eos = 1;
        } else if (ret < 0) {
            av_log(g_dec_ctx, AV_LOG_ERROR, "av_read_frame failed, ret(%d)\n", ret);
            goto fail;
        }

        if (packet.stream_index != video_stream_idx) {
            av_packet_unref(&packet);
            continue;
        }
        ret = avcodec_send_packet(dec_ctx, &packet);
        if (ret < 0) {
            av_log(dec_ctx, AV_LOG_ERROR,
                "send pkt failed, ret(%d), %s, %d\n", ret, __FILE__, __LINE__);
            goto fail;
        }
//这里就是最后循环取出解码器中的yuv数据
        while (ret >= 0 || eos) {
            ret = avcodec_receive_frame(dec_ctx, p_frame);
            if (ret == AVERROR_EOF) {
                av_log(g_dec_ctx, AV_LOG_INFO, "dec receive eos\n");
                av_frame_unref(p_frame);
                av_frame_free(&p_frame);
                return 0;
            } else if (ret == 0) {
                save_yuv_file(dec_ctx, p_frame);
                av_frame_unref(p_frame);
            } else if (ret < 0 && ret != AVERROR(EAGAIN)) {
                av_log(dec_ctx, AV_LOG_ERROR, "receive frame failed\n");
                goto fail;
            }
        }
        av_packet_unref(&packet);
    }

 fail:
    av_frame_free(&p_frame);
    return -1;
}

到这里为止,Pyav基础用法基本完成。接下来讲一下架构。
Packet类:
主要封装了AVPacket,提供了一些可以set/get packet成员的一些property,里面函数有to_bytes()可以将data数据转为bytes对象,另外还有一个decode(),是通过其内部stream的指针去指向codec,然后去解码。

序号Value
成员变量-1AVPacket* ptr
成员变量-2Stream _stream
Propertystream_index
Propertystream
Propertytime_base
Propertypts
Propertydts
Propertypos
Propertysize
Propertyis_keyframe
Propertyis_corrupt
Propertybuffer_size(也就是packet的datasize)
Propertybuffer_ptr
Propertyto_bytes(将data转为python的bytes)
Fundecode(self._stream.decode(self))
构造self.ptr = lib.av_packet_alloc()
析构lib.av_packet_free(&self.ptr)

Frame 类,这是一个基类,所以里面只有基础信息

序号Value
成员变量-1AVFrame *ptr
成员变量-2int index
成员变量-3AVRational _time_base
成员变量-3_SideDataContainer _side_data
Propertydts
Propertypts
Propertytime(The presentation time in seconds for this frame)
Propertytime_base(fractions.Fraction)
Propertyis_corrupt( Is this frame corrupt?)
Propertyside_data
构造self.ptr = lib.av_frame_alloc()
析构lib.av_frame_free(&self.ptr)

VideoFrame 类:
该类继承了Frame类,除了提供了获取avframe类中的变量外,还提供了几个函数,可以csc颜色空间转换,保存jpg,或者从jpg,nump数组中转为Frame.

序号Value
成员变量-1VideoReformatter reformatter
成员变量-2VideoFormat format
Propertywidth
Propertyheight
Propertykey_frame
Propertyinterlaced_frame
Propertypict_type
Propertyplanes
Funto_rgb()( return self.reformat(format=“rgb24”, **kwargs))
Funto_image(可以保存为jpg)
Funto_ndarray()
Funfrom_image(img)
Funfrom_ndarray()

Stream类:
在stream类中还包含了两个其它的类:ContainerCodecContext

序号Value
成员变量-1AVStream *ptr
成员变量-2Container container
成员变量-3CodecContext codec_context
成员变量-4dict metadata
Propertyid
Propertyprofile
Propertyindex
Propertyaverage_rate
Propertybase_rate
Propertyguessed_rate
Propertystart_time
Propertyduration
Propertyframes(The number of frames this stream contains.)
Propertylanguage
PropertyType( Examples: 'audio', 'video', 'subtitle'.)
Funencode()
Fundecode()
Funget()/det() att

ContainerFormat 类:
这个类封装了ffmpeg的两个结构体:AVInputFormatAVOutputFormat ,在类里提供了可以访问该结构体的一些属性,主要在后面的container类中使用

序号Value
成员变量-1AVInputFormat *iptr
成员变量-2AVOutputFormat *optr
Propertydescriptor
Propertyoptions
Propertyinput
Propertyoutput
Propertyis_input
Propertyis_output
Propertylong_name
Propertyextensions
Propertyflags
构造self.iptr = lib.av_find_input_format(name)/self.optr = lib.av_guess_format(name, NULL, NULL)
全局函数
全局函数get_output_format_names() 获取所有ffmpeg支持的output names
全局函数get_input_format_names()获取所有ffmpeg支持的input names

Container类:基类

序号Value
成员变量-1AVFormatContext *ptr
成员变量-2dict options
成员变量-3dict container_options
成员变量-4list stream_options
成员变量-5StreamContainer streams
成员变量-6open_timeout
成员变量-7read_timeout
成员变量-8io_open
Fundumps_format()
Funset_timeout() self.interrupt_callback_info.timeout = timeout
Funstart_timeout() self.interrupt_callback_info.start_time = clock()
构造函数avformat_open_input()
析构函数avformat_free_context(self.ptr)
全局函数open()根据flag是r还是w选择return InputContainer()/ return OutputContainer()

InputContainer(Container) 类:
InputContainer 这里有个创建codec的步骤,特别要注意,并且这里是采用avcodec_find_decoder()意味着你无法在这里选择合适的解码器,所以我们上面第三个案例中采用了demux和单独codec create()的做法

序号Value
成员变量-1AVStream *ptr
成员变量-2Container container
Fundemux()在里面对stream赋值packet._stream = self.streams[packet.ptr.stream_index],stream中包含了py_codec_context
Fundecode() 实际使用的是 packet.decode()
Funseek()
构造函数avformat_find_stream_info()->avcodec_find_decoder()->avcodec_alloc_context3()
析构函数avformat_close_input()

OutputContainer(Container) 类:

序号Value
成员变量-1bint _started
成员变量-2_done
Funadd_stream() 这里创建一个输出流avformat_new_stream()->创建编码器CodecContext py_codec_context = wrap_codec_context(codec_context, codec),注意这里的py_codec_context赋给stream中的相关变量,用来后期encode()
Funmux() av_interleaved_write_frame(self.ptr, self.packet_ptr)

Codec 类:
该类主要封装了AVCodec,提供了相关获取的属性

序号Value
成员变量-1AVCodec *ptr
成员变量-2AVCodecDescriptor *desc
Propertyis_decoder
Propertydescriptor
Propertyname
Propertytype
Propertyframe_rates
Propertyaudio_rates
Propertyvideo_formats
Propertyaudio_formats
Funcreate() 工厂函数return CodecContext.create(self)
构造函数avcodec_find_encoder_by_name() /avcodec_find_decoder_by_name根据mode是w/r选择解码器或者编码器
析构函数
全局函数cdef Codec wrap_codec(const lib.AVCodec *ptr)
全局函数get_codec_names() 所有ffmpeg支持的codec names
全局函数dump_codecs() ““Print information about available codecs.””

CodecContext类:

序号Value
成员变量-1AVCodecContext *ptr
成员变量-2bint extradata_set
成员变量-3int stream_index
成员变量-4Codec codec
成员变量-5dict options
Propertyflags
Propertyextradata set/get att 参见第三个sample中的设置
Propertyextradata_size
Propertyis_open
Propertyis_encoder
Propertyis_decoder
Propertyskip_frame
Propertythread_type
Propertythread_count
Propertybit_rate_tolerance
Propertymax_bit_rate
Propertybit_rate
Propertyticks_per_frame
Propertycodec_tag
Propertytime_base
Propertyprofile
Propertyname
Funcreate()工厂函数
Funopen()
Funclose()
Funencode()这里会调用一次self.open(strict=False),所以open()可以不用显示调用
Fundecode() 这里会调用一次self.open(strict=False),所以open()可以不用显示调用
Funparse 里面调用的是av_parser_parse2()
构造函数self.codec = wrap_codec() ->self.options = {} 这个options在第一个decode()调用之前可以设置,具体参见第三个sample
析构函数lib.avcodec_close(self.ptr)/lib.avcodec_free_context(&self.ptr)
全局函数CodecContext wrap_codec_context(lib.AVCodecContext*, const lib.AVCodec*)

VideoCodecContext(CodecContext) 类:
该类也没做什么,主要对外提供属性接口

序号Value
成员变量-1VideoFormat _format
成员变量-2VideoReformatter reformatter
成员变量-3int encoded_frame_count (for encoding)
成员变量-4VideoFrame next_frame (for decoding)
Propertyformat
Propertywidth
Propertyheight
Propertypix_fmt
Propertyframerate
Propertyrate
Propertygop_size
Propertysample_aspect_ratio
Propertydisplay_aspect_ratio
Propertyhas_b_frames
Propertycoded_width
Propertycoded_height
Fun_build_format()

VideoReformatter 类:
主要进行sws_cale的操作

序号Value
成员变量-1SwsContext *ptr
Funreformat()

其它辅助类:
VideoFormat 类:
该类主要封装了AVPixelFormatAVPixFmtDescriptor

序号Value
成员变量-1AVPixelFormat pix_fmt
成员变量-2AVPixFmtDescriptor *ptr
Propertyname
Propertybits_per_pixel
Propertypadded_bits_per_pixel
Propertyis_big_endian
Propertyhas_palette
Propertyis_bit_stream
Propertyis_planar
Propertyis_rgb
Funchroma_width
Funchroma_height
构造self.pix_fmt = pix_fmt ,self.ptr = lib.av_pix_fmt_desc_get(pix_fmt)

PyIOFile 类:

序号Value
成员变量-1AVIOContext *iocontext
成员变量-2unsigned char *buffer
成员变量-3long pos
Propertyfread
Propertyfwrite
Propertyfseek
Propertyftell
Propertyfclose
Funpyio_read()
Funpyio_write()
Funpyio_seek()
Fun
Fun

相关阅读

手机版|MSIPO技术圈 皖ICP备19022944号-2

Copyright © 2024, msipo.com

返回顶部