ffmpeg+python在windows平台和linux(ubuntu18.04)上的应用(2)

书接上回,由于上文的推流码率基本在20Mbps左右,在实际的工程中达不到这个要求。于是大哥说要降低码率至4Mbps左右才行,而且需要写错误log并且把整个程序打包好,可以通过一个python程序直接调用,综上所述,故有此文。

 

目录

ffmpeg调参

在Linux上安装lalserver

用一个python文件直接运行lalserver和推流代码

写Log文件


需要完成的三个事情:

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类里把函数实例化就行了吧。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值