基于SIP协议音视频通讯终端的测试:从工具选择到pyav的探索
原因

该公司的产品是基于SIP协议的音频和视频通信终端。最新产品需要执行音频和视频测试。在以前的工具中,仅支持音频返回测试。后来,执行了视频循环函数。最近的会议还谈论了音频和视频测试的测试。使用不同的编解码器进行测试以完成测试。我觉得我以前的循环不能用作良好的测试条件(因为它也涉及产品本身的音频和视频收集),因此有如此需要。
探索
实际上,在开始时,我没有选择使用ffmpeg,而是选择使用我忘了致电的库。无论如何它都不容易使用,也是基于C的库。后来,我要求使用ffmeg,所以我找到了ffmpeg-python和python-ffmeg。这两个库均基于FFMPEG-CLI,并且操作粒度不是很好。我搜索并找到了Pyav。
Pyav是用Cython编写的图书馆。它重新包装ffmeg对象用于Python。
但是,这些文件极为罕见,中国文件非常小。因此,只需通过触摸石头就过河。
需要
我的需要是打开本地文件,然后通过RTP流的形式通过本地指定端口将其发送到目标地址的目标端口。
有必要通过两个不同的端口同时发送视频和音频。
演示过程中使用的音频编解码器是PCMU,使用的视频编解码器为H264。
思维
在音频方面,因为我想达到20ms的ptime,time.sleep()无法在Windows系统下的毫秒内等待,因此需要在Linux系统中运行。为了实现更准确的20ms间隔软件包,它也需要经过专门设计。检查时间。
我在程序启动之前录制了一个standard_start,并在解码新框架之前使用了标准_start +20ms,将其用作标准解码启动时间,然后记录了解码之前的时间,而startup delays_time_time = trays_start -stant_start -standard_start是要计算的。 time.time() - 真正_start作为编解码器完成后使用的时间,因此剩余时间应为20ms -delay_time-如果大于零,等待,并且如果它小于零,则将立即进入下一个帧。代码解码过程。
在视频方面,因为编码器具有40架缓冲区(我不知道在哪里设置它,但这是正确的),所以您不应该等待您在编码器队列中的每个帧,但您应该等待等待编码器输出。开始等待。然后,在末尾,缓冲区队列中的40帧将由out_packets = self.output_stream.encode()取出,然后按下流。
演示
伪代码可能是:
//打开输入文件容器
input_ = av.open(in_file)
//打开输出容器
输出= av.open(rtp_url)
//添加输出流
output_stream = output.add_stream(编解码)
//解码输入框架
input_frame = input.decode(音频= 0)
//编码输出包
output_packets = output_stream.encode(input_frame)
//输出包
output.mux(output_packets)
class AVPlayer:
def __init__(self, use_port: int, res: str = 'test_30fps_8000hz.avi'):
self.remote_ip = ''
self.remote_port = None
self.use_port = use_port
self.res = os.path.join(os.path.dirname(__file__), res)
self._input = av.open(self.res, 'r')
self.url = ''
self.url_option = ''
self.codec = ''
self.rate = None
self._output = None
self.output_stream = None
self.resampler = None
self.is_config = False
self.is_sending = False
class MyAudioPlayer(AVPlayer):
def __init__(self, use_port, res: str = 'test_30fps_8000hz.avi'):
super().__init__(use_port, res)
self.channels = None
self.format = None
self.ptime = None
self.pkt_size = None
def config(self, aim_ip, aim_port, codec: str = 'pcmu', ptime: int = 20):
if codec == 'pcmu':
self.codec = 'pcm_mulaw'
self.rate = 8000
self.channels = 1
elif codec == 'pcma':
self.codec = 'pcm_alaw'
self.rate = 8000
self.channels = 1
else:
raise TypeError
if self.channels == 1:
self.format = "mono"
else:
self.format = "stereo"
self.ptime = ptime
self.pkt_size = self.channels * self.rate * self.ptime / 1000
self.url_option = f'&pkt_size={self.pkt_size + 12}'
self.url = f'rtp://{aim_ip}:{aim_port}?localrtpport={self.use_port}{self.url_option}'
self._output = av.open(self.url, 'w', format="rtp")
self.output_stream = self._output.add_stream(self.codec, self.rate)
self.output_stream.codec_context.channels = self.channels
self.resampler = AudioResampler("s16", self.format, self.rate, self.pkt_size)
self.is_config = True
self.is_sending = True
thread = threading.Thread(target=self._send)
thread.setDaemon(True)
thread.start()
def _send(self):
standard_start = time.time() # 标准启动时
pts = 0
while self.is_sending:
with av.open(self.res, 'r') as _input:
for input_frame in _input.decode(audio=0):
resample_frames = self.resampler.resample(input_frame)
for resample_frame in resample_frames:
out_packets = self.output_stream.encode(resample_frame)
if out_packets:
for out_packet in out_packets:
out_packet.pts = pts
out_packet.dts = pts
really_start = time.time() # 实际启动时
delay_time = really_start - standard_start # 启动偏差
self._output.mux(out_packet)
used = time.time() - really_start # 实际用时
ptime = (self.ptime / 1000) # 标准用时
if (delay_time + used) < ptime:
time.sleep(ptime - used - delay_time)
standard_start += ptime
pts += 160
_input.close()
class MyVideoPlayer(AVPlayer):
def __init__(self, use_port, res: str = 'test_30fps_noB_30gop_8000hz_20ms.avi'):
super().__init__(use_port, res)
self.stream_option = {}
self.pack_mode = None
self.option = {}
def config(self, aim_ip, aim_port, codec: str = 'h264', pack_mode: int = 1):
if codec == 'h264':
self.codec = 'libx264'
self.rate = 30
self.stream_option = {"crf": "25"}
else:
raise TypeError
if pack_mode == 0:
self.url_option = '&pkt_size=65535'
self.pack_mode = pack_mode
self.option = {"rtpflags": "h264_mode0"}
else:
self.url_option = ''
self.pack_mode = pack_mode
self.option = {}
self.url = f'rtp://{aim_ip}:{aim_port}?localrtpport={self.use_port}{self.url_option}'
self._output = av.open(self.url, "w", format="rtp", options=self.option)
self.output_stream = self._output.add_stream(self.codec, self.rate, options=self.stream_option)
self.output_stream.width = self._input.streams.video[0].codec_context.width
self.output_stream.height = self._input.streams.video[0].codec_context.height
self.output_stream.pix_fmt = self._input.streams.video[0].codec_context.pix_fmt
self._input.close()
self.is_config = True
self.is_sending = True
thread = threading.Thread(target=self._send)
thread.setDaemon(True)
thread.start()
def _send(self):
standard_start = time.time() # 标准启动时
pts = 0
while self.is_sending:
with av.open(self.res, 'r') as _input:
for input_frame in _input.decode(video=0):
pts += 1
input_frame.pts = pts
out_packets = self.output_stream.encode(input_frame)
if out_packets:
for out_packet in out_packets:
out_packet.dts = out_packet.pts
really_start = time.time() # 实际启动时
delay_time = really_start - standard_start # 启动偏差
self._output.mux(out_packet)
used = time.time() - really_start # 实际用时
ptime = 1 / self.rate # 标准用时
if (delay_time + used) < ptime:
time.sleep(ptime - used - delay_time)
standard_start += ptime
if not self.is_sending:
break
out_packets = self.output_stream.encode()
for out_packet in out_packets:
out_packet.dts = out_packet.pts
really_start = time.time() # 实际启动时
delay_time = really_start - standard_start # 启动偏差
self._output.mux(out_packet)
used = time.time() - really_start # 实际用时
ptime = 1 / self.rate # 标准用时
if (delay_time + used) < ptime:
time.sleep(ptime - used - delay_time)
standard_start += ptime
class Media:
def __init__(self):
self.res = os.path.join(os.path.dirname(__file__), 'test_30fps_8000hz.avi')
self.aim_ip = '10.20.0.13'
self.aim_a_port = 12100
self.aim_v_port = 12150
self.use_a_port = 12100
self.use_v_port = 12150
self.audio_codec = ''
self.audio_player = MyAudioPlayer(self.use_a_port)
self.video_player = MyVideoPlayer(self.use_v_port)
def send_all(self):
self.audio_player.config(self.aim_ip, self.aim_a_port)
self.video_player.config(self.aim_ip, self.aim_v_port)