书接上回,由于上文的推流码率基本在20Mbps左右,在实际的工程中达不到这个要求。于是大哥说要降低码率至4Mbps左右才行,而且需要写错误log并且把整个程序打包好,可以通过一个python程序直接调用,综上所述,故有此文。
目录
需要完成的三个事情:
1.在尽可能保证画质的前提下压缩码率
2.加入错误log
3.在linux打包python程序
接下来按照顺序进行处理
ffmpeg调参
没想到ffmpeg也要调参。。。。先上结论
在布谷鸟ADUS502上实测码率差不多4Mbps
def push_image(self):
command_out = ['ffmpeg',
'-y', # 覆盖输出文件不询问
'-f', 'rawvideo', # 输入格式
'-vcodec', 'rawvideo',
'-pix_fmt', 'bgr24', # 和后面那个加起来的意思是解码颜色再编码
'-s', str(self.width) + '*' + str(self.height), # 设置图像大小
'-r', str(self.fps), # 设置帧数
'-i', '-',
'-c:v', 'libx264', # 视频压缩格式
'-pix_fmt', 'yuv420p',
'-preset', 'superfast', # 输出的视频质量,零延迟下输出质量需要调整到最低ultrafast
'-tune', 'zerolatency', # 极大降低延迟,但画面容易中断,甚至打不开imshow命令
# '-b:v', '4000k', # 调整码率为10Mbps+
'-bufsize', '100000k', # 加缓冲
'-sc_threshold', '300', # 设置场景更改检测的阈值,值越高画面会越流畅,但是相同码率下画面会变模糊,值过低运动时画面会出现卡顿
'-profile:v', 'high', # 设置画面质量
'-g', '30',
'-crf', '28', # 数字越大,压缩越狠,画质越差
'-rtsp_transport', 'tcp',
'-f', 'rtsp',
部分参数解释:
"-preset", 有veryslow,slow,fast,superfast,ultrafast等档位,代表着解码速度,越快压缩越高,越低压缩越慢,帧率越低。当然我说的是针对实时rtsp流,如果只是完整视频转码应该越慢越好吧。
'-profile:v',有baseline,main等档位,代表画面质量,越高,压缩等级越高。
'-crf',从0-51越高压缩越狠,设置了这个就不用设置码率了,设置了也白设置。
在Linux上安装lalserver
直接从下面链接下载编译好的安装包
https://github.com/q191201771/lal/releases/tag/v0.30.1
下载这个,咱们用的边缘计算是ARM
下载之后是一个zip格式的压缩包
unzip filename.zip
解压之后文件夹改名为lalserver,进入目录能看到bin,conf,log
首先进入conf,打开lalserver.conf.json,加入鉴权信息
找到这个代码段,按照我这个改,在拉流的时候输入用户名和密码就靠这个
"rtsp": {
"enable": true,
"addr": ":5544",
"out_wait_key_frame_flag": true,
"auth_enable": true,
"auth_method": 1,
"username": "admin",
"password": "admin"
},
然后加入推流的鉴权信息
"simple_auth": {
"key": "123",
"dangerous_lal_secret": "123",
"pub_rtmp_enable": false,
"sub_rtmp_enable": false,
"sub_httpflv_enable": false,
"sub_httpts_enable": false,
"pub_rtsp_enable": true,
"sub_rtsp_enable": false,
"hls_m3u8_enable": false
},
推流的时候要输入的密码就靠这个了
之后退到lalserver的目录,运行终端,输入
./bin/lalserver -c conf/lalserver.conf.json
看见这个就说明开启了
测试结束,下一步要用python运行这行代码
用一个python文件直接运行lalserver和推流代码
先上代码
class Live_stack(object):
def __init__(self):
# 相关参数设置
# 摄像机地址
self.Camera_url = '/dev/video0'
# 推流服务器地址
self.Push_url = 'rtsp://'
# self.Push_url = 'rtsp://'
# 设置缓冲栈大小
self.stack_len = 100
# 设置缓冲栈
self.stack = []
# 接收主机屏幕分辨率
# 获得屏幕分辨率X轴
# self.width = win32api.GetSystemMetrics(win32con.SM_CXSCREEN)
self.width = 1920
# 获得屏幕分辨率Y轴
# self.height = win32api.GetSystemMetrics(win32con.SM_CYSCREEN)
self.height = 1080
# 获得屏幕刷新率
# self.fps = getattr(win32api.EnumDisplaySettings(win32api.EnumDisplayDevices().DeviceName, -1), 'DisplayFrequency')
self.fps = 30
# 保存端口信息
# 运行
def run_server(self):
print('推流端口未被占用,正在开启推流服务器')
command_out = ['lalserver -c /home/cookoo/push_stream/lalserver/conf/lalserver.conf.json']
sp.Popen(command_out, stdin=sp.PIPE, shell=True)
print('服务器开启')
# 查找端口是否被占用并写入文件
def search(self):
serach = 'lsof -i :5544 > /home/cookoo/push_stream/lalserver/logs/duankou.txt'
p = sp.Popen(serach, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.STDOUT, shell=True)
p.terminate()
# 结束这个进程
def run_linuxcmd(self):
# 先查询一下我们用的端口有没有占用
self.search()
# 在linux下,当shell = True时,如果arg是个字符串,就使用shell来解释执行这个字符串
# 如果args是个列表,则第一项被视为命令,其余的都视为是给shell本身的参数
# 端口占用信息存放地址
file_path = '/home/cookoo/push_stream/lalserver/logs/duankou.txt'
f = open(file_path, encoding='utf-8')
txt = []
for line in f:
txt.append(line.strip())
if len(txt) == 0:
self.run_server()
time.sleep(1)
self.search()
else:
txt = txt[1].split()
print('推流端口%s目前被%s占用,其PID为%s' % (txt[8], txt[0], txt[1]))
# 输入选择
while (1):
user_in = input('是否杀死该进程(Y/N)?')
access_all = ['Y', 'y', 'N', 'n']
access_No = ['N', 'n']
access_Yes = ['Y', 'y']
if user_in in access_all:
break
else:
print('请输入正确的选项')
if user_in in access_Yes:
software = str(txt[0])
command = 'killall' + software
sp.Popen(command, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.STDOUT, shell=True)
print('已杀死进程,准备开启服务器')
with open(file_path, 'a+', encoding='utf-8') as test:
test.truncate(0)
self.run_server()
print('yes')
elif user_in in access_No:
print('端口占用无法继续运行')
sys.exit()
# 从摄像机拉流放入缓冲栈
def read_image(self):
capture = cv2.VideoCapture(self.Camera_url)
capture.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G'))
try:
if (capture.isOpened()) == 0:
raise ValueError("摄像头未开启")
except ValueError as e:
print("引发异常", repr(e))
capture.set(cv2.CAP_PROP_FRAME_WIDTH, self.width) # 宽度
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height) # 高度
capture.set(cv2.CAP_PROP_FPS, self.fps) # 帧率
# 查看一下视频的大小以及帧率
fps = int(capture.get(cv2.CAP_PROP_FPS))
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
print('接收图像数据帧数为%s,宽度为%s,高度为%s' % (fps, width, height))
# 检查帧是否正常
while True:
ret, frame = capture.read()
try:
if ret == 0:
raise ValueError("读取帧错误")
except ValueError as e:
print("引发异常", repr(e))
# 写入缓冲栈
mutex.acquire()
self.stack.append(frame)
# print(stack)
# 每到一定容量清空一次缓冲栈
# 利用gc库,手动清理内存垃圾,防止内存溢出
if len(self.stack) >= self.stack_len:
del self.stack[:]
gc.collect()
mutex.release()
# 从缓冲栈推流到rtsp服务器
def push_image(self):
command_out = ['ffmpeg',
'-y', # 覆盖输出文件不询问
'-f', 'rawvideo', # 输入格式
'-vcodec', 'rawvideo',
'-pix_fmt', 'bgr24', # 和后面那个加起来的意思是解码颜色再编码
'-s', str(self.width) + '*' + str(self.height), # 设置图像大小
'-r', str(self.fps), # 设置帧数
'-i', '-',
'-c:v', 'libx264', # 视频压缩格式
'-pix_fmt', 'yuv420p',
'-preset', 'superfast', # 输出的视频质量,零延迟下输出质量需要调整到最低ultrafast
'-tune', 'zerolatency', # 极大降低延迟,但画面容易中断,甚至打不开imshow命令
# '-b:v', '4000k', # 调整码率为10Mbps+
'-bufsize', '100000k', # 加缓冲
'-sc_threshold', '300', # 设置场景更改检测的阈值,值越高画面会越流畅,但是相同码率下画面会变模糊,值过低运动时画面会出现卡顿
'-profile:v', 'high', # 设置画面质量
'-g', '30',
'-crf', '28', # 数字越大,压缩越狠,画质越差
'-rtsp_transport', 'tcp',
'-f', 'rtsp',
self.Push_url]
p = sp.Popen(command_out, stdin=sp.PIPE, shell=False)
# 判断缓冲栈是否已满或为空
while True:
mutex.acquire()
if len(self.stack) != 0:
if len(self.stack) != 100 and len(self.stack) != 0:
frame = self.stack.pop()
p.stdin.write(frame.tobytes())
else:
print('未接收到摄像机数据')
if len(self.stack) >= self.stack_len:
print('缓冲栈炸了')
del self.stack[:]
gc.collect()
mutex.release()
def run_single_camera(self):
self.run_linuxcmd()
time.sleep(1)
threads = [threading.Thread(target=Live_stack.read_image, args=(self,)), # 开一个进程,把rstp流数据接回来
threading.Thread(target=Live_stack.push_image, args=(self,))] # 处理接到的图像,再推到rtmp服务器上
[thread.start() for thread in threads] # 开启子进程
[thread.join() for thread in threads] # 等待所有子进程运行完毕
if __name__ == '__main__':
# 互斥锁
mutex = threading.Lock()
# 实例化
live = Live_stack()
live.run_single_camera()
实际使用的时候发现端口占用会导致服务器开启失败,于是就加了一段。代码仍然存在较大不足,比如有多个软件占用一个端口(lalserver和ffmpeg)我这里面只是把第一行的占用给杀了。还有就是lalserver这个服务器支持很多格式,我在这里给他配置文件里面除了rtsp其他的全部配置false。
一定要注意在search()里面把子进程结束了,不然当端口为空的时候他会往文件里写数据
就这样式儿的~
言归正传,查找端口占用程序
def search(self):
serach = 'lsof -i :5544 > /home/cookoo/push_stream/lalserver/logs/duankou.txt'
sp.Popen(serach, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.STDOUT, shell=True)
# 结束这个进程
将查找到的端口占用信息写入到txt文件里,然后通过读取文件的方式判断有没有占用
software = str(txt[0])
command = 'killall' + software
sp.Popen(command, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.STDOUT, shell=True)
print('已杀死进程,准备开启服务器')
这里就是调用killall命令杀死进程
之后要把txt文件清空
with open(file_path, 'a+', encoding='utf-8') as test:
test.truncate(0)
防止程序开头的self.search在没被占用的情况下判断为占用
一个小小的错误点:
不管是写txt文件还是读txt文件,编码格式这都是utf-8,不然虚拟机环境下可能可以顺利执行的程序在ARM可不一定。
写Log文件
写这个Log文件以前没有接触过,总而言之他就是一个可以记录你代码里面的错误/警告/致命错误的东西。
先上代码
class Logger(object):
def __init__(self, logger_name='logs'):
# 创建一个logger
self.logger = logging.getLogger(logger_name)
# logger的等级,这里是最低
logging.root.setLevel(logging.NOTSET)
self.log_file_name = 'test.log' # 日志文件的名称
self.backup_count = 5 # 最多存放日志的数量
# 日志输出级别
self.console_output_level = 'WARNING'
self.file_output_level = 'INFO'
# 日志输出格式/时间/名称/等级/信息
self.formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# 日志目录
self.path = './Logs/'
def get_logger(self):
"""在logger中添加日志句柄并返回,如果logger已有句柄,则直接返回"""
# 如果不存在路径文件夹就创建
if not os.path.exists(self.path):
os.makedirs(self.path)
if not self.logger.handlers: # 避免重复日志
# 定义一个句柄,能够将日志信息输出到sys.stdout, sys.stderr 或者类文件对象
console_handler = logging.StreamHandler()
# 设置 handler 中日志记录格式
console_handler.setFormatter(self.formatter)
# 输入到日志文件中的日志等级
console_handler.setLevel(self.console_output_level)
# 将日志输出到控制台
self.logger.addHandler(console_handler)
# 每天重新创建一个日志文件,最多保留backup_count份
file_handler = TimedRotatingFileHandler(filename=os.path.join(self.path, self.log_file_name), when='D',
interval=1, backupCount=self.backup_count, delay=True,
encoding='utf-8')
# 格式化日志文件
file_handler.setFormatter(self.formatter)
# 设置记录等级
file_handler.setLevel(self.file_output_level)
# 将日志写到文件里
self.logger.addHandler(file_handler)
return self.logger
可以设置把什么等级的信息写到控制台和文件中,等级区分如下
.DEBUG<INFO<WARNING<ERROR<CRITICAL
先说一下怎么用好了,本来我就是想找别人的代码直接用的()。
INFO的话需要你在代码里写一句,然后设置正确的等级就可以写到log文件里
logging.info('你想记录的信息')
警告没试过,错误的话想要写入文件需要把代码段放到try....except下
# 实例化你的log类,调用函数
logger = Logger().get_logger()
try:
# 互斥锁
mutex = threading.Lock()
# 实例化
live = Live_stack()
live.run_single_camera()
except Exception as e:
# 这里需要import fraceback包
logger.error(str(traceback.format_exc()))
在你代码里随便找一个地方写一个print(4/0)就能看见写入文件和控制台的错误了。我这个是和推流程序在一个python文件里,如果你放在一个工程文件的两个.py文件下只需要在需要log记录的文件头import你的log类,并在你的log类里把函数实例化就行了吧。