From ec121715422855083d5392b45febfb6838bb9aa7 Mon Sep 17 00:00:00 2001 From: chenyukun <764784960@qq.com> Date: Mon, 16 Jan 2023 16:44:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airport_application.yml | 25 ++ airport_master.py | 11 + common/Constant.py | 20 + common/__init__.py | 0 common/__pycache__/Constant.cpython-38.pyc | Bin 0 -> 795 bytes common/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 137 bytes concurrency/CommonThread.py | 22 + concurrency/__init__.py | 0 .../__pycache__/CommonThread.cpython-38.pyc | Bin 0 -> 985 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 142 bytes enums/ExceptionEnum.py | 13 + enums/__init__.py | 0 .../__pycache__/ExceptionEnum.cpython-38.pyc | Bin 0 -> 597 bytes enums/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 136 bytes exception/CustomerException.py | 19 + exception/__init__.py | 0 .../CustomerException.cpython-38.pyc | Bin 0 -> 723 bytes exception/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 140 bytes logs/airport.log | 203 +++++++++ service/PushStreamService.py | 72 ++++ service/__init__.py | 0 .../PushStreamService.cpython-38.pyc | Bin 0 -> 1620 bytes service/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 138 bytes test/ffmpeg_test.py | 249 +++++++++++ test/strtest.py | 10 + util/Cv2Utils.py | 394 ++++++++++++++++++ util/LogUtils.py | 27 ++ util/TimeUtils.py | 19 + util/YmlUtils.py | 21 + util/__init__.py | 0 util/__pycache__/Cv2Utils.cpython-38.pyc | Bin 0 -> 10088 bytes util/__pycache__/LogUtils.cpython-38.pyc | Bin 0 -> 784 bytes util/__pycache__/YmlUtils.cpython-38.pyc | Bin 0 -> 879 bytes util/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 135 bytes 34 files changed, 1105 insertions(+) create mode 100644 airport_application.yml create mode 100644 airport_master.py create mode 100644 common/Constant.py create mode 100644 common/__init__.py create mode 100644 common/__pycache__/Constant.cpython-38.pyc create mode 100644 common/__pycache__/__init__.cpython-38.pyc create mode 100644 concurrency/CommonThread.py create mode 100644 concurrency/__init__.py create mode 100644 concurrency/__pycache__/CommonThread.cpython-38.pyc create mode 100644 concurrency/__pycache__/__init__.cpython-38.pyc create mode 100644 enums/ExceptionEnum.py create mode 100644 enums/__init__.py create mode 100644 enums/__pycache__/ExceptionEnum.cpython-38.pyc create mode 100644 enums/__pycache__/__init__.cpython-38.pyc create mode 100644 exception/CustomerException.py create mode 100644 exception/__init__.py create mode 100644 exception/__pycache__/CustomerException.cpython-38.pyc create mode 100644 exception/__pycache__/__init__.cpython-38.pyc create mode 100644 logs/airport.log create mode 100644 service/PushStreamService.py create mode 100644 service/__init__.py create mode 100644 service/__pycache__/PushStreamService.cpython-38.pyc create mode 100644 service/__pycache__/__init__.cpython-38.pyc create mode 100644 test/ffmpeg_test.py create mode 100644 test/strtest.py create mode 100644 util/Cv2Utils.py create mode 100644 util/LogUtils.py create mode 100644 util/TimeUtils.py create mode 100644 util/YmlUtils.py create mode 100644 util/__init__.py create mode 100644 util/__pycache__/Cv2Utils.cpython-38.pyc create mode 100644 util/__pycache__/LogUtils.cpython-38.pyc create mode 100644 util/__pycache__/YmlUtils.cpython-38.pyc create mode 100644 util/__pycache__/__init__.cpython-38.pyc diff --git a/airport_application.yml b/airport_application.yml new file mode 100644 index 0000000..d59425e --- /dev/null +++ b/airport_application.yml @@ -0,0 +1,25 @@ +video: + pullUrl: "rtsp://127.0.0.1:8554/video" + pushUrl: ["rtmp://192.168.10.101:19350/rlive/stream_9?sign=f8a15b6n"] +# 日志设置 +log: + # 是否开启文件输出 True:开启 False:关闭 + enable_file_log: True + # 是否开启控制台日志输出 True:开启 False:关闭 + enable_stderr: True + # 日志打印文件夹 + base_path: ./logs/ + # 日志文件名称 + log_name: airport.log + # 日志打印格式 + log_fmt: "{time:YYYY-MM-DD HH:mm:ss.SSS} [{level}][{process.name}-{process.id}-{thread.name}-{thread.id}][{line}] {module}-{function} - {message}" + # 日志隔离级别 + level: INFO + # 日志每天0点创建新文件 + rotation: 00:00 + # 日志保存时间1天 + retention: 1 days + # 线程安全 + enqueue: True + # 编码格式 + encoding: utf8 diff --git a/airport_master.py b/airport_master.py new file mode 100644 index 0000000..04163e1 --- /dev/null +++ b/airport_master.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +from alg_airport_ffmpeg.service.PushStreamService import PushStreamService + +""" + airport主程序入口 +""" + + +if __name__ == '__main__': + print("(♥◠‿◠)ノ゙ AIRPORT【推流服务】开始启动 ლ(´ڡ`ლ)゙") + PushStreamService().start_service() diff --git a/common/Constant.py b/common/Constant.py new file mode 100644 index 0000000..f00e255 --- /dev/null +++ b/common/Constant.py @@ -0,0 +1,20 @@ +from enum import Enum, unique + + +# 常量枚举 +@unique +class ConstantEnum(Enum): + APPLICATION_CONFIG = ("airport_application.yml", "配置文件名称") + + UTF_8 = ("utf-8", "utf-8") + + R = ("r", "可读") + + START_LOG = (""" + _ ___ ____ ____ ___ ____ _____ + / \ |_ _| _ \| _ \ / _ \| _ \_ _| + / _ \ | || |_) | |_) | | | | |_) || | + / ___ \ | || _ <| __/| |_| | _ < | | + /_/ \_\___|_| \_\_| \___/|_| \_\|_| + :: AIRPORT SERVICE :: (1.0.0.RELEASE) +""", "启动服务LOG") diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/common/__pycache__/Constant.cpython-38.pyc b/common/__pycache__/Constant.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7647a6892e17fca2498c5d0f5355069e742ea86c GIT binary patch literal 795 zcmYjPzi-n(6uvv#Aw-Fay09>KL8=E5*s3T(je;9Rijv4l7%WStBWF;Af22;Vswi7Q zNR=WwF|jZK3t?iZ5dVi)NK5~PPP}&pweRfjzI*q*@9yjyS1Kg}F@E-Qa-R|M3kNsD zfWaQT^Bn+5Bn?PNT^cf%QR1^UkxXi@iPU_0tQ}KVllqWY#wDnU#WLNF(l9He(exno zCz!k$6b5_n&hG*E#HEtBOri;Mb*Z_A)ZKzKz_5^&TX8gBh-iV17JGG9Op{rhEV!7> zg6Tvorg2m~3WIbTye{9YE`EGHfB*LP+4s%q>f+Nki(MJ%;`xK!1WyDzqe)r-wt2Vy zvp%z!*n-WJObo7obCW~BP$72zI3|EAan1Bha|zNV)_FOawQG)MFo|N71+}?^hg(Q; zM3!?T${Dn$M$HC}@vk9$yp)5y>U2$tj->`+{g`kf@zWalR@-j5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HXi&ac{eo=Nz zVorK|VqzkM&MYd(FDi*i&d<%w&x?tV&&Iy;u|n=DX*g}HsS=*2sG{ajs6-JuqW|T!CLZ(b&1~-t!x1b$2@TH+`Hr?VDm}nB&5_+2bmfwy(NWIc3Z0)D5=#mEWe=doA2Z>!Ql7)f!`p{QK>v z!}&#v-{*O?(0QJR{}Rtz?B+$=6g^_u6qoamR%rFX`6Ui%H>>qTi#?I}?`jqt54IH* zf}FO(HaG2v)IK9v!5!(DxEItiIFl!U2~DSwQ)lh6!4VB`La-J%Wt)=@@SH3+UX+~< zmCtBwJwsw5uJ$)+{9XUwM2IEHENv1&t`DCua31)A;h!ub>>EzU(p3kLSL01^nofmx zCccT|Ca+84c&v5Zn!aLtqVg`k0>3Z&lR@-j5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!Hqi&ac{eo=Nz zVorK|VqzkM&MYd(FDi*i&d*COEhg`kg7l32$pMTE439w^WWWmKH~?|629QW$NMX!j$YqRT%w>vVVq{2Va%V_k zN?~qcNMTN8Y-Vm|jABk<31-k_eF;>}pviQL#Wk-q_ZC}eUS?rwYBER=82~MW7%l;1 zq%uS?rZ7Y?rGN}!iegD&j$(xvc8k}wB0067Br`uRq_QAYlPQYTF~GpUpi~m5?OAu{ zv#pI!=WKX7x8cc--WSb#pX}K6eB~}hm@-3%vc47glv!F@mKp<1d%k)1^WF2Gwza-& zUh#BK)6*S0bQE5!-~OU;3Q*?h_NC8PbU)qE@pRp+CwqIJE@^o_p-s_GlkFBqe0*MF zZfbn|Ew1?Z-29Z%91xo)KEALtF$XFm84&8@6CWHBp_#6$?4SyYf; zR1%X4jDg}9cnE?bM6aN-hy|z{6uHH0K!Ssrg^7g;Eas=la*G9|r-%t;J~M~_sYO@^ YVQ~Y+ZgJQ^JZlGv$zqT)0Tw1k0FxNEF8}}l literal 0 HcmV?d00001 diff --git a/enums/__pycache__/__init__.cpython-38.pyc b/enums/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3f7813dd7809b762adbfa10dccc91f1a2c54d54 GIT binary patch literal 136 zcmWIL<>g`kg4;>^lR@-j5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!Hni&ac{eo=Nz zVorK|VqzkM&MYd(FDi*i%`43f0U$_Eua6*Fty<%BZQ&d$t^-@Z5A*K2DlK-SGJvsVJ(7blArQnE|S zZjjTUMFFMg3jr@d2U?zjmL~Wj`%;JdkVL;C!MAy4UcArDxX!ClCh4P@am;9YwCp;$iI{+bNkE=Z2|@Id zz=c_*jgzGvCenr06bHzXFe3K@aEZ3R-RIrGy9(b7(qgF6G;Q}B#}(ECGrN7T*Vwu$ z4bJ&&jX$_pseF{zNTkoC{4t4oSwL{4NE} zoTLQ3W|K~@rk}4aE^ek@Z_YpcnSML@_-vysU7wxZUjFo@Bt#yIiwt6gcVZKP9jbf9 zt&6#BNhw>SQV%Jy{2_?r$M=x;O}!*y!K1V^O1TxK%1So{$E!*mHEGeF;4;A=4`kss uo;326$bXI>ou5`L;?P%mN$6iqgH0=7c|K=g`k0>R|{$sqbMh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6wz#VV#ezbHE< zF(*AfF)jX93oK(AypL=EmbeoQn6iiYprBOv6IfOCzIMU z0YXwyTEPKP1c#nCTnZ8rTo64}LP-1-bA?2^x1Kri>||G2cJXNJ_xwITzh{5XTh(eA zL3`o+AFTtB{xZs9b3nNQzoyV}#IZ!wj6~vuP=bx@NGEZrYh_n@iBJ6`pn=sr873uK zva&DBNrhH0x`ueb!-t564+xDo-bD4%A^1ldbzDU2X;){>Zg#bsC!p6hh3dyGarNVt z=w)%*t&<|Woye^$mbwV9ryVN+(wAwcBNRx%djF!CZ5gMFjev3me*H3Z0#VEnB^-0| z5K)Ia+=bEQ9t7kU%Qtd;XERfxnao3IVOVw4jBHM49e#ZQx(w~(F&ZL9GH2f%qFpk^ zCU0(1`V~`glGG=|V^WaXK*bpg`!G+V{ zBjAbbL%i6yUwo5@hUQz!wGnttO7q)(Fh-FE@&k4hj>UHkdedj$Q^8F z!G17CBX8*JheJHZdSLuCjNe=6Ex4bNf6w1Wx4{#jYk+7yT}zuuob`+V~BsWH}9ln>K|8+Y5O zGGwNu&71Fic#~cK;9BAJn=%)w1k)<8uq;jlJr`?cV`AEpx$v|UqDPloGSz~a@a7h} z421{$IWYyR7j%~HDZ~206H2A3a1z}qO2@TXc-Cn>RE8WhG>5=)CDoEuTm6eHOH)ZB zBU%1IkyFpa`DTH2frX{_tc%P()mcb$E}J^jZ$O9e3R%KSxQtywaOjkA1ab(UOF|Oj zs8++T;2K`TgI90-w~iYpd_rxSYUnl-!B|mdEJ=ATjT|v{H*d~=E`g-g-#gQnR->Nz$AUmmg$M|Y_!rONxO}s=WceLjWdAa=;dpy=Q>Lhp^nen z-UWT}j72#-fA-0lJtXFB%vEmbL)ZQX-g`kg23yg$sqbMh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o2DX#VV#ezbHE< zF(*AfF)4$7kkcmc+;F6;$5hu*uC&Da}c>0~z!gh#3H; Cl^@gq literal 0 HcmV?d00001 diff --git a/test/ffmpeg_test.py b/test/ffmpeg_test.py new file mode 100644 index 0000000..c1d59a3 --- /dev/null +++ b/test/ffmpeg_test.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- +import time +import cv2 +import subprocess as sp + +import ffmpeg +import numpy as np +from loguru import logger + + + + + +''' + 获取视频信息 +''' +def get_video_info(pullUrl): + try: + probe = ffmpeg.probe(pullUrl) + if probe is None or probe.get("streams") is None: + return + # 视频大小 + # format = probe['format'] + # size = int(format['size'])/1024/1024 + video_stream = next((stream for stream in probe['streams'] if stream.get('codec_type') == 'video'), None) + if video_stream is None: + logger.error("根据拉流地址未获取到视频流") + return + width = video_stream.get('width') + height = video_stream.get('height') + nb_frames = video_stream.get('nb_frames') + fps = video_stream.get('r_frame_rate') + # duration = video_stream.get('duration') + bit_rate = video_stream.get('bit_rate') + if width: + width = int(width) + if height: + height = int(height) + if nb_frames: + all_frames = int(nb_frames) + if fps: + up, down = str(fps).split('/') + fps = int(eval(up) / eval(down)) + # if duration: + # self.duration = float(video_stream['duration']) + if bit_rate: + bit_rate = int(bit_rate) / 1000 + logger.info("视频信息, width:{}|height:{}|fps:{}|all_frames:{}|bit_rate:{}", width, + height, fps, all_frames, bit_rate) + except ffmpeg._run.Error as er: + logger.error("获取视频信息异常: {}", er.stderr.decode(encoding='utf-8')) + except Exception as e: + logger.exception("获取视频信息异常:{}", e) + + +''' + 拉取视频 +''' +def build_pull_p(wh, pullUrl): + try: + command = [r'E:\liumeiti\ffmpeg\ffmpeg-master-latest-win64-gpl\bin\ffmpeg', + '-re', + '-y', + '-c:v', 'h264_cuvid', + '-resize', wh, + '-i', pullUrl, + '-f', 'rawvideo', + '-an', + '-'] + return sp.Popen(command, stdout=sp.PIPE) + except Exception as e: + logger.exception("构建拉流管道异常:{}", e) + return None + +def read(whr, whrz, pullUrl, h, w): + result = None + try: + in_bytes = build_pull_p(whrz, pullUrl).stdout.read(whr) + if in_bytes is not None and len(in_bytes) > 0: + # result = (np.frombuffer(in_bytes, np.uint8).reshape([int(self.height), int(self.width), 3])) + try: + img = (np.frombuffer(in_bytes, np.uint8)).reshape((h, w)) + except Exception as ei: + logger.exception("视频格式异常:{}", ei) + result = cv2.cvtColor(img, cv2.COLOR_YUV2BGR_NV12) + except Exception as e: + logger.exception("读流异常:{}", e) + return result + +def close(self): + if self.pull_p: + if self.pull_p.stdout: + self.pull_p.stdout.close() + self.pull_p.terminate() + self.pull_p.wait() + logger.info("关闭拉流管道完成, requestId:{}", self.requestId) + if self.p: + if self.p.stdin: + self.p.stdin.close() + self.p.terminate() + self.p.wait() + # self.p.communicate() + # self.p.kill() + logger.info("关闭管道完成, requestId:{}", self.requestId) + if self.or_video_file: + self.or_video_file.release() + logger.info("关闭原视频写入流完成, requestId:{}", self.requestId) + if self.ai_video_file: + self.ai_video_file.release() + logger.info("关闭AI视频写入流完成, requestId:{}", self.requestId) + + + +# async def push_stream_write(self, frame): +# self.p.stdin.write(frame.tostring()) +# +# async def push_stream(self, frame): +# if self.p is None: +# self.build_p() +# try: +# await self.push_stream_write(frame) +# return True +# except Exception as ex: +# logger.exception("推流进管道异常:{}, requestId: {}", ex, self.requestId) +# current_retry_num = 0 +# while True: +# try: +# time.sleep(1) +# self.p_push_retry_num += 1 +# current_retry_num += 1 +# if current_retry_num > 3 or self.p_push_retry_num > 600: +# return False +# self.build_p() +# await self.push_stream_write(frame) +# logger.info("构建p管道重试成功, 当前重试次数: {}, requestId: {}", current_retry_num, +# self.requestId) +# return True +# except Exception as e: +# logger.exception("构建p管道异常:{}, 开始重试, 当前重试次数:{}, requestId: {}", e, +# current_retry_num, self.requestId) +# return False + +# async def video_frame_write(self, or_frame, ai_frame): +# if or_frame is not None: +# self.or_video_file.write(or_frame) +# if ai_frame is not None: +# self.ai_video_file.write(ai_frame) + +# async def video_write(self, or_frame, ai_frame): +# try: +# self.build_write() +# if or_frame is not None and len(or_frame) > 0: +# await self.video_frame_write(or_frame, None) +# if ai_frame is not None and len(ai_frame) > 0: +# await self.video_frame_write(None, ai_frame) +# return True +# except Exception as ex: +# ai_retry_num = 0 +# while True: +# try: +# ai_retry_num += 1 +# if ai_retry_num > 3: +# logger.exception("重新写入离线分析后视频到本地,重试失败:{}, requestId: {}", e, self.requestId) +# return False +# if or_frame is not None and len(or_frame) > 0: +# await self.or_video_file.write(or_frame) +# if ai_frame is not None and len(ai_frame) > 0: +# await self.ai_video_file.write(ai_frame) +# logger.info("重新写入离线分析后视频到本地, 当前重试次数: {}, requestId: {}", ai_retry_num, +# self.requestId) +# return True +# except Exception as e: +# logger.exception("重新写入离线分析后视频到本地:{}, 开始重试, 当前重试次数:{}, requestId: {}", e, +# ai_retry_num, self.requestId) + +# def build_write(self): +# try: +# if self.fps is None or self.width is None or self.height is None: +# raise ServiceException(ExceptionType.VIDEO_CONFIG_EXCEPTION.value[0], +# ExceptionType.VIDEO_CONFIG_EXCEPTION.value[1]) +# if self.orFilePath is not None and self.or_video_file is None: +# self.or_video_file = cv2.VideoWriter(self.orFilePath, cv2.VideoWriter_fourcc(*'mp4v'), self.fps, +# (int(self.wn), int(self.hn))) +# if self.or_video_file is None: +# raise ServiceException(ExceptionType.OR_WRITE_OBJECT_EXCEPTION.value[0], +# ExceptionType.OR_WRITE_OBJECT_EXCEPTION.value[1]) +# if self.aiFilePath is not None and self.ai_video_file is None: +# self.ai_video_file = cv2.VideoWriter(self.aiFilePath, cv2.VideoWriter_fourcc(*'mp4v'), self.fps, +# (int(self.width), int(self.hn))) +# if self.ai_video_file is None: +# raise ServiceException(ExceptionType.AI_WRITE_OBJECT_EXCEPTION.value[0], +# ExceptionType.AI_WRITE_OBJECT_EXCEPTION.value[1]) +# except ServiceException as s: +# logger.exception("构建文件写对象异常: {}, requestId:{}", s, self.requestId) +# raise s +# except Exception as e: +# logger.exception("构建文件写对象异常: {}, requestId:{}", e, self.requestId) +# raise e + +# def video_merge(self, frame1, frame2): +# # frameLeft = cv2.resize(frame1, (int(self.width / 2), int(self.height / 2)), interpolation=cv2.INTER_LINEAR) +# # frameRight = cv2.resize(frame2, (int(self.width / 2), int(self.height / 2)), interpolation=cv2.INTER_LINEAR) +# # frame_merge = np.hstack((frameLeft, frameRight)) +# frame_merge = np.hstack((frame1, frame2)) +# return frame_merge +# +# def getP(self): +# if self.p is None: +# logger.error("获取管道为空, requestId:{}", self.requestId) +# raise ServiceException(ExceptionType.PULL_PIPELINE_INIT_EXCEPTION.value[0], +# ExceptionType.PULL_PIPELINE_INIT_EXCEPTION.value[1]) +# return self.p +# +# def getCap(self): +# if self.cap is None: +# logger.error("获取cv2为空, requestId:{}", self.requestId) +# raise ServiceException(ExceptionType.CV2_IS_NULL_EXCEPTION.value[0], +# ExceptionType.CV2_IS_NULL_EXCEPTION.value[1]) +# return self.cap +# +# def getOrVideoFile(self): +# if self.or_video_file is None: +# logger.error("获取原视频写入对象为空, requestId:{}", self.requestId) +# raise ServiceException(ExceptionType.OR_WRITE_OBJECT_EXCEPTION.value[0], +# ExceptionType.OR_WRITE_OBJECT_EXCEPTION.value[1]) +# return self.or_video_file +# +# def getAiVideoFile(self): +# if self.ai_video_file is None: +# logger.error("获取AI视频写入对象为空, requestId:{}", self.requestId) +# raise ServiceException(ExceptionType.AI_WRITE_OBJECT_EXCEPTION.value[0], +# ExceptionType.AI_WRITE_OBJECT_EXCEPTION.value[1]) +# return self.ai_video_file +if __name__== "__main__": + command = ['ffmpeg', + '-rtsp_transport', 'tcp', + '-i', 'rtsp://127.0.0.1:8554/video', # 指定输入文件 + '-c', 'copy', + '-f', 'flv', + "rtmp://192.168.10.101:19350/rlive/stream_9?sign=f8a15b6n"] + p = sp.Popen(command, shell=False) + while True: + time.sleep(2) + + print("pid", p.pid) + print("poll", p.poll()) + print("returncode", p.returncode) + # p.terminate() + # p.wait() diff --git a/test/strtest.py b/test/strtest.py new file mode 100644 index 0000000..241bc5f --- /dev/null +++ b/test/strtest.py @@ -0,0 +1,10 @@ + + +print(""" + _ ___ ____ ____ ___ ____ _____ + / \ |_ _| _ \| _ \ / _ \| _ \_ _| + / _ \ | || |_) | |_) | | | | |_) || | + / ___ \ | || _ <| __/| |_| | _ < | | + /_/ \_\___|_| \_\_| \___/|_| \_\|_| + :: AIRPORT SERVICE :: (1.0.0.RELEASE) +""") \ No newline at end of file diff --git a/util/Cv2Utils.py b/util/Cv2Utils.py new file mode 100644 index 0000000..0afa7fe --- /dev/null +++ b/util/Cv2Utils.py @@ -0,0 +1,394 @@ +# -*- coding: utf-8 -*- +import json +import subprocess as sp +import time + +import cv2 +import numpy as np +from loguru import logger + +from alg_airport_ffmpeg.enums.ExceptionEnum import ExceptionType +from alg_airport_ffmpeg.exception.CustomerException import ServiceException +from alg_airport_ffmpeg.concurrency.CommonThread import Common + +""" + 推流工具 +""" + + +class Cv2Util: + + def __init__(self, pullUrl=None, pushUrl=None): + self.__pullUrl = pullUrl + self.__pushUrl = pushUrl + self.__push_stream = None + self.__pull_stream = None + self.__width = None + self.__height = None + self.__wh = None + self.__fps = None + self.__cap = None + + def probe(self): + p = None + try: + args = ['ffprobe', '-show_format', '-show_streams', '-of', 'json', self.__pullUrl] + p = sp.Popen(args, stdout=sp.PIPE, stderr=sp.PIPE, close_fds=True) + out, err = p.communicate(timeout=7) + if p.returncode != 0: + # logger.error("获取视频信息异常: {}", err.stderr.decode(encoding='utf-8')) + return None + return json.loads(out.decode('utf-8')) + except Exception as e: + # logger.error("获取视频信息异常: {}", e) + return None + finally: + if p: + # if p.stdout: + # p.stdout.flush() + # p.stdout.close() + # if p.stderr: + # p.stderr.close() + p.terminate() + # parent_proc = psutil.Process(p.pid) + # for child_proc in parent_proc.children(recursive=True): + # child_proc.kill() + # parent_proc.kill() + # p.kill() + p.wait() + # logger.info("关闭获取视频管道完成!") + + # 获取视频信息 + def get_video_info(self): + try: + if self.__pullUrl is None or len(self.__pullUrl) == 0: + raise ServiceException(ExceptionType.PULL_STREAM_URL_EXCEPTION.value[0], + ExceptionType.PULL_STREAM_URL_EXCEPTION.value[1]) + probe = self.probe() + if probe is None or probe.get("streams") is None: + return + video_stream = next((stream for stream in probe['streams'] if stream.get('codec_type') == 'video'), None) + if video_stream is None: + return + width = video_stream.get('width') + height = video_stream.get('height') + fps = video_stream.get('r_frame_rate') + self.__width = int(width) + self.__height = int(height) + self.__wh = int(width * height * 3) + up, down = str(fps).split('/') + self.__fps = int(eval(up) / eval(down)) + logger.info("视频信息, width:{}|height:{}|fps:{}", self.__width, self.__height, self.__fps) + except ServiceException as s: + # logger.error("获取视频信息异常: {}", s.msg) + raise s + except Exception as e: + # logger.error("获取视频信息异常:{}", e) + raise e + + def build_cap(self, args): + try: + pullUrl = args[0] + return cv2.VideoCapture(pullUrl) + except Exception as e: + logger.error("初始化cap异常: {}", e) + return None + + # 构建 cv2 + def build_cv2(self): + try: + if self.__cap is not None: + # logger.info("重试, 关闭cap") + self.__cap.release() + self.__cap = None + self.__fps = None + self.__width = None + self.__height = None + if self.__pullUrl is None or len(self.__pullUrl) == 0: + raise ServiceException(ExceptionType.PULL_STREAM_URL_EXCEPTION.value[0], + ExceptionType.PULL_STREAM_URL_EXCEPTION.value[1]) + cap_thread = Common(timeout=7, func=self.build_cap, args=(self.__pullUrl,)) + cap_thread.setDaemon(True) + cap_thread.start() + self.__cap = cap_thread.get_result() + if self.__cap is None: + return + if self.__cap.isOpened(): + if self.__fps is None or self.__fps == 0: + self.__fps = int(self.__cap.get(cv2.CAP_PROP_FPS)) + if self.__width is None or self.__width == 0: + self.__width = int(self.__cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + if self.__height is None or self.__height == 0: + self.__height = int(self.__cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + logger.info("fps:{}|height:{}|width:{}", self.__fps, self.__height, self.__width) + except ServiceException as s: + logger.error("构建cv2异常: {}", s.msg) + raise s + except Exception as e: + logger.error("初始化cv2异常:{}", e) + raise e + + def cv2_read(self): + result = None + try: + if self.__cap is None: + self.build_cv2() + if self.__cap.isOpened(): + ret, frame = self.__cap.read() + if ret: + result = frame + del ret + del frame + except ServiceException as s: + raise s + except Exception as e: + logger.error("读流异常:{}", e) + raise e + finally: + if result is None: + self.__fps = None + self.__height = None + self.__width = None + if self.__cap: + self.__cap.release() + self.__cap = None + logger.info("关闭cv2!") + return result + + # 拉取视频 + def build_pull_stream(self): + try: + if self.__pullUrl is None or len(self.__pullUrl) == 0: + logger.error("拉流地址不能为空!") + raise ServiceException(ExceptionType.PULL_STREAM_URL_EXCEPTION.value[0], + ExceptionType.PULL_STREAM_URL_EXCEPTION.value[1]) + if self.__pull_stream: + logger.info("重试, 关闭拉流管道") + if self.__pull_stream.stdout: + self.__pull_stream.stdout.close() + self.__pull_stream.terminate() + self.__pull_stream.wait() + command = ['ffmpeg', + '-rtsp_transport', 'tcp', + '-i', self.__pullUrl, + '-f', 'rawvideo', + '-pix_fmt', 'bgr24', + '-an', + '-'] + # command = ['ffmpeg', + # '-re', + # '-y', + # '-c:v', 'h264_cuvid', + # '-resize', self.wah, + # '-i', self.pullUrl, + # '-f', 'rawvideo', + # '-an', + # '-'] + self.__pull_stream = sp.Popen(command, stdout=sp.PIPE) + except ServiceException as s: + logger.error("构建拉流管道异常: {}", s.msg) + raise s + except Exception as e: + logger.error("构建拉流管道异常:{}", e) + raise e + + def check_config(self): + if self.__fps is None or self.__width is None or self.__height is None: + return True + else: + return False + + def read(self): + result = None + try: + if self.__pull_stream is None: + self.build_pull_stream() + in_bytes = self.__pull_stream.stdout.read(self.__wh) + if in_bytes is not None and len(in_bytes) > 0: + result = (np.frombuffer(in_bytes, np.uint8).reshape([int(self.__height), int(self.__width), 3])) + except ServiceException as s: + raise s + except Exception as e: + logger.error("读流异常:{}", e) + raise e + finally: + if result is None: + self.__fps = None + self.__height = None + self.__width = None + if self.__pull_stream: + if self.__pull_stream.stdout: + self.__pull_stream.stdout.close() + self.__pull_stream.terminate() + self.__pull_stream.wait() + logger.info("关闭拉流管道完成!") + self.__pull_stream = None + return result + + # 关闭管道 + def close(self): + if self.__pull_stream: + if self.__pull_stream.stdout: + self.__pull_stream.stdout.close() + self.__pull_stream.terminate() + self.__pull_stream.wait() + logger.info("关闭拉流管道完成!") + if self.__push_stream: + if self.__push_stream.stdin: + self.__push_stream.stdin.close() + self.__push_stream.terminate() + self.__push_stream.wait() + logger.info("关闭推流管道完成!") + if self.__cap: + self.__cap.release() + + # 开始推流 + def build_push_stream(self): + try: + if self.__push_stream: + logger.info("重试, 关闭管道, 重新开启新管道") + if self.__push_stream.stdin: + self.__push_stream.stdin.close() + self.__push_stream.terminate() + self.__push_stream.wait() + if self.__pushUrl is None or len(self.__pushUrl) == 0: + logger.error("推流地址不能为空!") + raise ServiceException(ExceptionType.PUSH_STREAM_URL_EXCEPTION.value[0], + ExceptionType.PUSH_STREAM_URL_EXCEPTION.value[1]) + command = ['ffmpeg', + # '-loglevel', 'debug', + '-f', 'rawvideo', + '-vcodec', 'rawvideo', + '-pix_fmt', 'bgr24', + '-s', "{}x{}".format(int(self.__width), int(self.__height)), + '-r', str(self.__fps), + '-i', '-', + # '-g', str(self.__fps), + # '-maxrate', '15000k', + # '-minrate', '3000k', + # '-profile:v', 'high', + # '-level', '5.1', + # '-b:v', '4000k', + # '-crf', '26', + # '-bufsize', '4000k', + # '-c:v', 'libx264', + # '-tune', 'zerolatency', + # '-sc_threshold', '0', + # '-pix_fmt', 'yuv420p', + # "-an", + # '-preset', 'medium', # 指定输出的视频质量,会影响文件的生成速度,有以下几个可用的值 ultrafast, + # superfast, veryfast, faster, fast, medium, slow, slower, veryslow。 + ] + for url in self.__pushUrl: + command.extend(['-f', 'flv', + '-g', str(self.__fps), + '-maxrate', '15000k', + '-minrate', '3000k', + '-b:v', '4000k', + '-bufsize', '4000k', + '-c:v', 'libx264', + '-tune', 'zerolatency', + '-sc_threshold', '0', + '-pix_fmt', 'yuv420p', + '-preset', 'fast', + "-an", "-y", url + ]) + self.__push_stream = sp.Popen(command, stdin=sp.PIPE, shell=False) + except ServiceException as s: + logger.error("构建推流管道异常: {}", s.msg) + raise s + except Exception as e: + logger.error("初始化推流管道异常:{}", e) + raise e + + def push_stream_write(self, frame): + try: + if self.__push_stream is None: + self.build_push_stream() + self.__push_stream.stdin.write(frame.tostring()) + except Exception as ex: + logger.error("推流异常:{}", ex) + current_retry_num = 0 + while True: + try: + self.build_push_stream() + self.__push_stream.stdin.write(frame.tostring()) + logger.info("推流重试成功, 当前重试次数: {}", current_retry_num) + break + except Exception as e: + current_retry_num += 1 + logger.error("推流异常:{}, 开始重试, 当前重试次数:{}", e, current_retry_num) + time.sleep(1) + if current_retry_num > 1: + raise Exception("推流异常,请检查通道是否被占用!") + + def close_push_stream(self): + if self.__push_stream: + self.__push_stream.terminate() + self.__push_stream.wait() + self.__push_stream = None + + def is_push_stream_ok(self): + if self.__push_stream: + if self.__push_stream.poll() is not None: + return True + return False + + # 开始推流 + def start_push_stream(self): + try: + if self.__push_stream: + return + if self.__pullUrl is None or len(self.__pullUrl) == 0: + logger.error("拉流地址不能为空!") + raise ServiceException(ExceptionType.PUll_STREAM_URL_EXCEPTION.value[0], + ExceptionType.PUll_STREAM_URL_EXCEPTION.value[1]) + if self.__pushUrl is None or len(self.__pushUrl) == 0: + logger.error("推流地址不能为空!") + raise ServiceException(ExceptionType.PUSH_STREAM_URL_EXCEPTION.value[0], + ExceptionType.PUSH_STREAM_URL_EXCEPTION.value[1]) + command = ['ffmpeg', + '-re', + '-rtsp_transport', 'tcp', + '-i', self.__pullUrl, + ] + for url in self.__pushUrl: + command.extend(['-f', 'flv', + '-g', str(25), + '-c:v', 'copy', + "-an", "-y", url + ]) + self.__push_stream = sp.Popen(command, shell=False) + except ServiceException as s: + logger.error("构建推流管道异常: {}", s.msg) + raise s + except Exception as e: + logger.error("初始化推流管道异常:{}", e) + raise e + + def is_video_stream(self, url): + p = None + try: + if url is None or len(url) == 0: + raise Exception("流地址不能为空!") + args = ['ffprobe', '-show_format', '-show_streams', '-of', 'json', url] + p = sp.Popen(args, stdout=sp.PIPE, stderr=sp.PIPE, close_fds=True) + out, err = p.communicate(timeout=7) + if p.returncode != 0: + return False + probe = json.loads(out.decode('utf-8')) + if probe is None or probe.get("streams") is None: + return False + video_stream = next((stream for stream in probe['streams'] if stream.get('codec_type') == 'video'), None) + if video_stream is None: + return False + return True + except ServiceException as s: + raise s + except Exception as e: + return False + finally: + if p: + p.terminate() + p.wait() + p = None diff --git a/util/LogUtils.py b/util/LogUtils.py new file mode 100644 index 0000000..b301679 --- /dev/null +++ b/util/LogUtils.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +import sys +import os +from loguru import logger + + +# 初始化日志配置 +def init_log(content): + if not os.path.exists(content["log"]["base_path"]): + os.makedirs(content["log"]["base_path"]) + # 移除日志设置 + logger.remove(handler_id=None) + # 打印日志到文件 + if content["log"]["enable_file_log"]: + logger.add(content["log"]["base_path"] + content["log"]["log_name"], + rotation=content["log"]["rotation"], + retention=content["log"]["retention"], + format=content["log"]["log_fmt"], + level=content["log"]["level"], + enqueue=content["log"]["enqueue"], + encoding=content["log"]["encoding"]) + # 控制台输出 + if content["log"]["enable_stderr"]: + logger.add(sys.stderr, + format=content["log"]["log_fmt"], + level=content["log"]["level"], + enqueue=True) diff --git a/util/TimeUtils.py b/util/TimeUtils.py new file mode 100644 index 0000000..89adac5 --- /dev/null +++ b/util/TimeUtils.py @@ -0,0 +1,19 @@ +import time +import datetime + +YY_MM_DD_HH_MM_SS = "%Y-%m-%d %H:%M:%S" +YMDHMSF = "%Y%m%d%H%M%S%f" + + +def generate_timestamp(): + """根据当前时间获取时间戳,返回整数""" + return int(time.time()) + + +def now_date_to_str(fmt=None): + if fmt is None: + fmt = YY_MM_DD_HH_MM_SS + return datetime.datetime.now().strftime(fmt) + +if __name__=="__main__": + print(now_date_to_str(YMDHMSF)) diff --git a/util/YmlUtils.py b/util/YmlUtils.py new file mode 100644 index 0000000..5f06c71 --- /dev/null +++ b/util/YmlUtils.py @@ -0,0 +1,21 @@ +import os +import yaml +from alg_airport_ffmpeg.common.Constant import ConstantEnum + +""" + 获取配置项信息 +""" + + +def get_configs(): + print("开始读取配置文件,获取配置信息:", ConstantEnum.APPLICATION_CONFIG.value[0]) + config_path = os.path.abspath(ConstantEnum.APPLICATION_CONFIG.value[0]) + if not os.path.exists(config_path): + raise Exception("未找到配置文件:{}".format(ConstantEnum.APPLICATION_CONFIG.value[0])) + with open(config_path, ConstantEnum.R.value[0], encoding=ConstantEnum.UTF_8.value[0]) as f: + file_content = f.read() + content = yaml.load(file_content, yaml.FullLoader) + if not content: + raise Exception("配置项不能为空:{}".format(ConstantEnum.APPLICATION_CONFIG.value[0])) + print("读取配置文件完成!") + return content diff --git a/util/__init__.py b/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/util/__pycache__/Cv2Utils.cpython-38.pyc b/util/__pycache__/Cv2Utils.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63a7ac3c43b4eaeb30e653e7461e7029b6db1c93 GIT binary patch literal 10088 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 literal 0 HcmV?d00001 diff --git a/util/__pycache__/LogUtils.cpython-38.pyc b/util/__pycache__/LogUtils.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2aedad42b9c47626733586057e62d68be1ddc20a GIT binary patch literal 784 zcmY*XO>5LZ7@o=JWSg~8)UuvM42VBKiU@j?UbL665T-k`X|_pbeP>cFc=Wn|z*9Z? zS3GzX=IY5`AQS}O$)@Q#VV?Kv;r+^-Os5lqhyUyre|$oIIb-*DXuQO$pCAY#Xif61 zq&Y>~tG&k0eM*Ecg3lxm1bs`g@Heu_aYk)atFn}^pw2NIrzdz{;nhD73|Y}D?-OR9 zVa6HxO7&wyuKZmOB_>#fSkd25N`^=V$IDNE@E=VTTx3WDv|HY zmY>$LSXKzg%o#$_@*wiHX0o}kpv)|Zx5ZDrgtTtE5|0-^WlftPwIeMl#_-*v=N%6i-T3x~+dLO4JHDMJ6? gI_Ua#`)J?H4m@xycGM`*)$)a#O1srD%$WxN09BN>?f?J) literal 0 HcmV?d00001 diff --git a/util/__pycache__/YmlUtils.cpython-38.pyc b/util/__pycache__/YmlUtils.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e17d8d8c3914cc0bcebf12a2c696ded27f54455 GIT binary patch literal 879 zcmZuvO-~a+7@l`O+CmEoQH**qp6rE~7!zZRkyKC&Dac3Erb)AQwsq@%Wp`E~w24@a z7(qM{Lp-$cVDJE*6rpJR8`S`ku!V?h@TF(m?_B9)$yltf{S#N{2VCdoMPSi&-0rkJeX z^o*>Z{oN!azIOt|cZRxVs5RdBDf2o)=B`{&E> z7+8CeU@qFyK^AKnJu|@=l`CzwV8dx#sMv-P%mNi_5m7nauHDm3Q}zj0_DX`o;%`lbOVDa$xWp zmmes)r*gq^xnwKs9uF&b-GGGDNzG+0Z|k4Tsy5RslZSGaVG}{d)rm`~Xx1`wT0Rr#LM|1{X}X%hBvVbs!+T_zI7e}t zKhIsgn0{zE_tT1=&nSx0ikf3v4$I_nhOOq)9@F&n9YdeM==R!EJepUTuZT9T|H<4Q z1OnY~1Y*zwryv5IAR&u@a|~zVvJ=MObRqE5fzttp-mGO9mf4%w`^~`(1}lS2?gQV5 VM=lu_^>p>J|G;htA%ae^@GrOR7?=P6 literal 0 HcmV?d00001 diff --git a/util/__pycache__/__init__.cpython-38.pyc b/util/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1989150abe27fed4854e2c8efe7b66df4b09b003 GIT binary patch literal 135 zcmWIL<>g`kg4pY&$sqbMh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o2D9#VV#ezbHE< zF(*AfF)lIYq;;_lhPbtkwwF8;*8HgDGMA06q literal 0 HcmV?d00001