選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

529 行
25KB

  1. # -*- coding: utf-8 -*-
  2. import json
  3. import time
  4. import cv2
  5. import subprocess as sp
  6. import numpy as np
  7. from loguru import logger
  8. from exception.CustomerException import ServiceException
  9. from enums.ExceptionEnum import ExceptionType
  10. class Cv2Util():
  11. def __init__(self, pullUrl, pushUrl=None, orFilePath=None, aiFilePath=None, requestId=None):
  12. self.pullUrl = pullUrl
  13. self.pushUrl = pushUrl
  14. self.orFilePath = orFilePath
  15. self.aiFilePath = aiFilePath
  16. self.cap = None
  17. self.p = None
  18. self.or_video_file = None
  19. self.ai_video_file = None
  20. self.fps = None
  21. self.width = None
  22. self.height = None
  23. self.wah = None
  24. self.wh = None
  25. self.h = None
  26. self.hn = None
  27. self.w = None
  28. self.all_frames = None
  29. self.bit_rate = None
  30. self.pull_p = None
  31. self.requestId = requestId
  32. self.p_push_retry_num = 0
  33. self.resize_status = False
  34. self.current_frame = 0
  35. def getFrameConfig(self, fps, width, height):
  36. if self.fps is None:
  37. self.fps = fps
  38. self.width = width
  39. self.height = height
  40. if width > 1600:
  41. self.wh = int(width * height * 3 // 8)
  42. self.wah = '%sx%s' % (int(self.width / 2), int(self.height / 2))
  43. self.h = int(self.height * 3 // 4)
  44. self.w = int(self.width // 2)
  45. self.hn = int(self.height // 2)
  46. self.wn = int(self.width // 2)
  47. w_f = self.wh != width * height * 3 / 8
  48. h_f = self.h != self.height * 3 / 4
  49. wd_f = self.w != self.width / 2
  50. if w_f or h_f or wd_f:
  51. self.resize_status = True
  52. self.wh = int(width * height * 3 // 2)
  53. self.wah = '%sx%s' % (int(self.width), int(self.height))
  54. self.h = int(self.height * 3 // 2)
  55. self.w = int(self.width)
  56. else:
  57. self.wh = int(width * height * 3 // 2)
  58. self.wah = '%sx%s' % (int(self.width), int(self.height))
  59. self.h = int(self.height * 3 // 2)
  60. self.w = int(self.width)
  61. self.hn = int(self.height)
  62. self.wn = int(self.width)
  63. def clear_video_info(self):
  64. self.fps = None
  65. self.width = None
  66. self.height = None
  67. '''
  68. 获取视频信息
  69. '''
  70. def get_video_info(self):
  71. try:
  72. if self.pullUrl is None:
  73. logger.error("拉流地址不能为空, requestId:{}", self.requestId)
  74. raise ServiceException(ExceptionType.PULL_STREAM_URL_EXCEPTION.value[0],
  75. ExceptionType.PULL_STREAM_URL_EXCEPTION.value[1])
  76. args = ['ffprobe', '-show_format', '-show_streams', '-of', 'json', self.pullUrl]
  77. p = sp.Popen(args, stdout=sp.PIPE, stderr=sp.PIPE)
  78. out, err = p.communicate(timeout=20)
  79. if p.returncode != 0:
  80. raise Exception("未获取视频信息!!!!!requestId:" + self.requestId)
  81. probe = json.loads(out.decode('utf-8'))
  82. if probe is None or probe.get("streams") is None:
  83. raise Exception("未获取视频信息!!!!!requestId:" + self.requestId)
  84. # 视频大小
  85. # format = probe['format']
  86. # size = int(format['size'])/1024/1024
  87. video_stream = next((stream for stream in probe['streams'] if stream.get('codec_type') == 'video'), None)
  88. if video_stream is None:
  89. raise Exception("未获取视频信息!!!!!requestId:" + self.requestId)
  90. width = video_stream.get('width')
  91. height = video_stream.get('height')
  92. nb_frames = video_stream.get('nb_frames')
  93. fps = video_stream.get('r_frame_rate')
  94. # duration = video_stream.get('duration')
  95. # bit_rate = video_stream.get('bit_rate')
  96. self.width = int(width)
  97. self.height = int(height)
  98. if width > 1600:
  99. self.wh = int(width * height * 3 // 8)
  100. self.wah = '%sx%s' % (int(self.width / 2), int(self.height / 2))
  101. self.h = int(self.height * 3 // 4)
  102. self.w = int(self.width / 2)
  103. self.hn = int(self.height / 2)
  104. self.wn = int(self.width // 2)
  105. w_f = self.wh != width * height * 3 / 8
  106. h_f = self.h != self.height * 3 / 4
  107. wd_f = self.w != self.width / 2
  108. if w_f or h_f or wd_f:
  109. self.resize_status = True
  110. self.wh = int(width * height * 3 // 2)
  111. self.wah = '%sx%s' % (int(self.width), int(self.height))
  112. self.h = int(self.height * 3 // 2)
  113. self.w = int(self.width)
  114. else:
  115. self.wh = int(width * height * 3 // 2)
  116. self.wah = '%sx%s' % (int(self.width), int(self.height))
  117. self.h = int(self.height * 3 // 2)
  118. self.w = int(self.width)
  119. self.hn = int(self.height)
  120. self.wn = int(self.width)
  121. if nb_frames:
  122. self.all_frames = int(nb_frames)
  123. up, down = str(fps).split('/')
  124. self.fps = int(eval(up) / eval(down))
  125. # if duration:
  126. # self.duration = float(video_stream['duration'])
  127. # self.bit_rate = int(bit_rate) / 1000
  128. logger.info("视频信息, width:{}|height:{}|fps:{}|all_frames:{}|bit_rate:{}, requestId:{}", self.width,
  129. self.height, self.fps, self.all_frames, self.bit_rate, self.requestId)
  130. except ServiceException as s:
  131. logger.error("获取视频信息异常: {}, requestId:{}", s.msg, self.requestId)
  132. self.clear_video_info()
  133. raise s
  134. except Exception as e:
  135. logger.exception("获取视频信息异常:{}, requestId:{}", e, self.requestId)
  136. self.clear_video_info()
  137. '''
  138. 拉取视频
  139. '''
  140. def build_pull_p(self):
  141. try:
  142. if self.wah is None:
  143. return
  144. if self.pull_p:
  145. logger.info("重试, 关闭拉流管道, requestId:{}", self.requestId)
  146. self.pull_p.stdout.close()
  147. self.pull_p.terminate()
  148. self.pull_p.wait()
  149. # command = ['ffmpeg',
  150. # # '-b:v', '3000k',
  151. # '-i', self.pullUrl,
  152. # '-f', 'rawvideo',
  153. # '-vcodec', 'rawvideo',
  154. # '-pix_fmt', 'bgr24',
  155. # # '-s', "{}x{}".format(int(width), int(height)),
  156. # '-an',
  157. # '-']
  158. # input_config = {'c:v': 'h264_cuvid', 'resize': self.wah}
  159. # process = (
  160. # ffmpeg
  161. # .input(self.pullUrl, **input_config)
  162. # .output('pipe:', format='rawvideo', r=str(self.fps)) # pix_fmt='bgr24'
  163. # .overwrite_output()
  164. # .global_args('-an')
  165. # .run_async(pipe_stdout=True)
  166. # )
  167. command = ['ffmpeg',
  168. '-re',
  169. '-y',
  170. '-c:v', 'h264_cuvid',
  171. '-resize', self.wah,
  172. '-i', self.pullUrl,
  173. '-f', 'rawvideo',
  174. '-an',
  175. '-']
  176. self.pull_p = sp.Popen(command, stdout=sp.PIPE)
  177. # self.pull_p = sp.Popen(command, stdout=sp.PIPE, stderr=sp.PIPE)
  178. # self.pull_p = process
  179. except ServiceException as s:
  180. logger.exception("构建拉流管道异常: {}, requestId:{}", s, self.requestId)
  181. if self.pull_p:
  182. logger.info("重试, 关闭拉流管道, requestId:{}", self.requestId)
  183. self.pull_p.stdout.close()
  184. self.pull_p.terminate()
  185. self.pull_p.wait()
  186. self.pull_p = None
  187. raise s
  188. except Exception as e:
  189. logger.exception("构建拉流管道异常:{}, requestId:{}", e, self.requestId)
  190. if self.pull_p:
  191. logger.info("重试, 关闭拉流管道, requestId:{}", self.requestId)
  192. self.pull_p.stdout.close()
  193. self.pull_p.terminate()
  194. self.pull_p.wait()
  195. self.pull_p = None
  196. def checkconfig(self):
  197. if self.fps is None or self.width is None or self.height is None:
  198. return True
  199. return False
  200. def read(self):
  201. result = None
  202. try:
  203. # if self.pull_p is None:
  204. # logger.error("拉流管道为空, requestId:{}", self.requestId)
  205. # raise ServiceException(ExceptionType.PULL_PIPELINE_INIT_EXCEPTION.value[0],
  206. # ExceptionType.PULL_PIPELINE_INIT_EXCEPTION.value[1])
  207. in_bytes = self.pull_p.stdout.read(self.wh)
  208. self.current_frame += 1
  209. if in_bytes is not None and len(in_bytes) > 0:
  210. # result = (np.frombuffer(in_bytes, np.uint8).reshape([int(self.height), int(self.width), 3]))
  211. try:
  212. img = (np.frombuffer(in_bytes, np.uint8)).reshape((self.h, self.w))
  213. except Exception as ei:
  214. logger.exception("视频格式异常:{}, requestId:{}", ei, self.requestId)
  215. raise ServiceException(ExceptionType.VIDEO_RESOLUTION_EXCEPTION.value[0],
  216. ExceptionType.VIDEO_RESOLUTION_EXCEPTION.value[1])
  217. result = cv2.cvtColor(img, cv2.COLOR_YUV2BGR_NV12)
  218. if self.resize_status:
  219. if self.width > 1600:
  220. result = cv2.resize(result, (int(self.width / 2), int(self.height / 2)),
  221. interpolation=cv2.INTER_LINEAR)
  222. except ServiceException as s:
  223. raise s
  224. except Exception as e:
  225. logger.exception("读流异常:{}, requestId:{}", e, self.requestId)
  226. return result
  227. def close(self):
  228. if self.pull_p:
  229. if self.pull_p.stdout:
  230. self.pull_p.stdout.close()
  231. self.pull_p.terminate()
  232. self.pull_p.wait()
  233. logger.info("关闭拉流管道完成, requestId:{}", self.requestId)
  234. if self.p:
  235. if self.p.stdin:
  236. self.p.stdin.close()
  237. self.p.terminate()
  238. self.p.wait()
  239. # self.p.communicate()
  240. # self.p.kill()
  241. logger.info("关闭管道完成, requestId:{}", self.requestId)
  242. if self.or_video_file:
  243. self.or_video_file.release()
  244. logger.info("关闭原视频写入流完成, requestId:{}", self.requestId)
  245. if self.ai_video_file:
  246. self.ai_video_file.release()
  247. logger.info("关闭AI视频写入流完成, requestId:{}", self.requestId)
  248. # 构建 cv2
  249. # def build_cv2(self):
  250. # try:
  251. # if self.cap is not None:
  252. # logger.info("重试, 关闭cap, requestId:{}", self.requestId)
  253. # self.cap.release()
  254. # if self.p is not None:
  255. # logger.info("重试, 关闭管道, requestId:{}", self.requestId)
  256. # self.p.stdin.close()
  257. # self.p.terminate()
  258. # self.p.wait()
  259. # if self.pullUrl is None:
  260. # logger.error("拉流地址不能为空, requestId:{}", self.requestId)
  261. # raise ServiceException(ExceptionType.PULL_STREAM_URL_EXCEPTION.value[0],
  262. # ExceptionType.PULL_STREAM_URL_EXCEPTION.value[1])
  263. # if self.pushUrl is None:
  264. # logger.error("推流地址不能为空, requestId:{}", self.requestId)
  265. # raise ServiceException(ExceptionType.PUSH_STREAM_URL_EXCEPTION.value[0],
  266. # ExceptionType.PUSH_STREAM_URL_EXCEPTION.value[1])
  267. # self.cap = cv2.VideoCapture(self.pullUrl)
  268. # if self.fps is None or self.fps == 0:
  269. # self.fps = int(self.cap.get(cv2.CAP_PROP_FPS))
  270. # if self.width is None or self.width == 0:
  271. # self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
  272. # if self.height is None or self.height == 0:
  273. # self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
  274. # command = ['/usr/bin/ffmpeg',
  275. # # '-y', # 不经过确认,输出时直接覆盖同名文件。
  276. # '-f', 'rawvideo',
  277. # '-vcodec', 'rawvideo',
  278. # '-pix_fmt', 'bgr24', # 显示可用的像素格式
  279. # # '-s', "{}x{}".format(self.width * 2, self.height),
  280. # '-s', "{}x{}".format(int(self.width), int(self.height/2)),
  281. # # '-r', str(15),
  282. # '-i', '-', # 指定输入文件
  283. # '-g', '25',
  284. # '-b:v', '3000k',
  285. # '-tune', 'zerolatency', # 加速编码速度
  286. # '-c:v', 'libx264', # 指定视频编码器
  287. # '-sc_threshold', '0',
  288. # '-pix_fmt', 'yuv420p',
  289. # '-an',
  290. # '-preset', 'ultrafast', # 指定输出的视频质量,会影响文件的生成速度,有以下几个可用的值 ultrafast,
  291. # # superfast, veryfast, faster, fast, medium, slow, slower, veryslow。
  292. # '-f', 'flv',
  293. # self.pushUrl]
  294. # # 管道配置
  295. # logger.info("fps:{}|height:{}|width:{}|requestId:{}", self.fps, self.height, self.width, self.requestId)
  296. # self.p = sp.Popen(command, stdin=sp.PIPE)
  297. # except ServiceException as s:
  298. # logger.exception("构建cv2异常: {}, requestId:{}", s, self.requestId)
  299. # raise s
  300. # except Exception as e:
  301. # logger.exception("初始化cv2异常:{}, requestId:{}", e, self.requestId)
  302. # 构建 cv2
  303. def build_p(self):
  304. try:
  305. if self.p:
  306. logger.info("重试, 关闭管道, requestId:{}", self.requestId)
  307. if self.p.stdin:
  308. self.p.stdin.close()
  309. self.p.terminate()
  310. self.p.wait()
  311. # self.p.communicate()
  312. # self.p.kill()
  313. if self.pushUrl is None:
  314. logger.error("推流地址不能为空, requestId:{}", self.requestId)
  315. raise ServiceException(ExceptionType.PUSH_STREAM_URL_EXCEPTION.value[0],
  316. ExceptionType.PUSH_STREAM_URL_EXCEPTION.value[1])
  317. width = int(self.width)
  318. if width <= 1600:
  319. width = 2 * int(self.width)
  320. command = ['ffmpeg',
  321. # '-loglevel', 'debug',
  322. '-y',
  323. '-f', 'rawvideo',
  324. '-vcodec', 'rawvideo',
  325. '-pix_fmt', 'bgr24',
  326. '-thread_queue_size', '1024',
  327. # '-s', "{}x{}".format(self.width * 2, self.height),
  328. '-s', "{}x{}".format(width, int(self.hn)),
  329. '-r', str(self.fps),
  330. '-i', '-', # 指定输入文件
  331. '-g', str(self.fps),
  332. '-maxrate', '8000k',
  333. # '-profile:v', 'high',
  334. '-b:v', '5000k',
  335. # '-crf', '18',
  336. # '-rc:v', 'vbr',
  337. # '-cq:v', '25',
  338. # '-qmin', '25',
  339. # '-qmax', '25',
  340. '-c:v', 'h264_nvenc', #
  341. '-bufsize', '5000k',
  342. # '-c:v', 'libx264', # 指定视频编码器
  343. # '-tune', 'zerolatency', # 加速编码速度
  344. # '-sc_threshold', '0',
  345. '-pix_fmt', 'yuv420p',
  346. "-an",
  347. # '-flvflags', 'no_duration_filesize',
  348. # '-preset', 'fast', # 指定输出的视频质量,会影响文件的生成速度,有以下几个可用的值 ultrafast,
  349. '-preset', 'p6', # 指定输出的视频质量,会影响文件的生成速度,有以下几个可用的值 ultrafast,
  350. '-tune', 'll',
  351. '-f', 'flv',
  352. self.pushUrl]
  353. # command = 'ffmpeg -loglevel debug -y -f rawvideo -vcodec rawvideo -pix_fmt bgr24' +\
  354. # ' -s ' + "{}x{}".format(int(self.width), int(self.height/2))\
  355. # + ' -i - ' + '-g ' + str(self.fps)+\
  356. # ' -b:v 6000k -tune zerolatency -c:v libx264 -pix_fmt yuv420p -preset ultrafast'+\
  357. # ' -f flv ' + self.pushUrl
  358. # kwargs = {'format': 'rawvideo',
  359. # # 'vcodec': 'rawvideo',
  360. # 'pix_fmt': 'bgr24',
  361. # 's': '{}x{}'.format(int(self.width), int(self.height/2))}
  362. # out = {
  363. # 'r': str(self.fps),
  364. # 'g': str(self.fps),
  365. # 'b:v': '5500k', # 恒定码率
  366. # # 'maxrate': '15000k',
  367. # # 'crf': '18',
  368. # 'bufsize': '5500k',
  369. # 'tune': 'zerolatency', # 加速编码速度
  370. # 'c:v': 'libx264', # 指定视频编码器
  371. # 'sc_threshold': '0',
  372. # 'pix_fmt': 'yuv420p',
  373. # # 'flvflags': 'no_duration_filesize',
  374. # 'preset': 'medium', # 指定输出的视频质量,会影响文件的生成速度,有以下几个可用的值 ultrafast,
  375. # # superfast, veryfast, faster, fast, medium, slow, slower, veryslow。
  376. # 'format': 'flv'}
  377. # 管道配置
  378. # process2 = (
  379. # ffmpeg
  380. # .input('pipe:', **kwargs)
  381. # .output(self.pushUrl, **out)
  382. # .global_args('-y', '-an')
  383. # .overwrite_output()
  384. # .run_async(pipe_stdin=True)
  385. # )
  386. logger.info("fps:{}|height:{}|width:{}|requestId:{}", self.fps, self.height, self.width, self.requestId)
  387. self.p = sp.Popen(command, stdin=sp.PIPE, shell=False)
  388. # self.p = process2
  389. except ServiceException as s:
  390. logger.exception("构建p管道异常: {}, requestId:{}", s, self.requestId)
  391. raise s
  392. except Exception as e:
  393. logger.exception("初始化p管道异常:{}, requestId:{}", e, self.requestId)
  394. async def push_stream_write(self, frame):
  395. self.p.stdin.write(frame.tostring())
  396. async def push_stream(self, frame):
  397. if self.p is None:
  398. self.build_p()
  399. try:
  400. await self.push_stream_write(frame)
  401. return True
  402. except Exception as ex:
  403. logger.exception("推流进管道异常:{}, requestId: {}", ex, self.requestId)
  404. current_retry_num = 0
  405. while True:
  406. try:
  407. time.sleep(1)
  408. self.p_push_retry_num += 1
  409. current_retry_num += 1
  410. if current_retry_num > 3 or self.p_push_retry_num > 600:
  411. return False
  412. self.build_p()
  413. await self.push_stream_write(frame)
  414. logger.info("构建p管道重试成功, 当前重试次数: {}, requestId: {}", current_retry_num,
  415. self.requestId)
  416. return True
  417. except Exception as e:
  418. logger.exception("构建p管道异常:{}, 开始重试, 当前重试次数:{}, requestId: {}", e,
  419. current_retry_num, self.requestId)
  420. return False
  421. async def video_frame_write(self, or_frame, ai_frame):
  422. if or_frame is not None:
  423. self.or_video_file.write(or_frame)
  424. if ai_frame is not None:
  425. self.ai_video_file.write(ai_frame)
  426. async def video_write(self, or_frame, ai_frame):
  427. try:
  428. self.build_write()
  429. if or_frame is not None and len(or_frame) > 0:
  430. await self.video_frame_write(or_frame, None)
  431. if ai_frame is not None and len(ai_frame) > 0:
  432. await self.video_frame_write(None, ai_frame)
  433. return True
  434. except Exception as ex:
  435. ai_retry_num = 0
  436. while True:
  437. try:
  438. ai_retry_num += 1
  439. if ai_retry_num > 3:
  440. logger.exception("重新写入离线分析后视频到本地,重试失败:{}, requestId: {}", e, self.requestId)
  441. return False
  442. if or_frame is not None and len(or_frame) > 0:
  443. await self.or_video_file.write(or_frame)
  444. if ai_frame is not None and len(ai_frame) > 0:
  445. await self.ai_video_file.write(ai_frame)
  446. logger.info("重新写入离线分析后视频到本地, 当前重试次数: {}, requestId: {}", ai_retry_num,
  447. self.requestId)
  448. return True
  449. except Exception as e:
  450. logger.exception("重新写入离线分析后视频到本地:{}, 开始重试, 当前重试次数:{}, requestId: {}", e,
  451. ai_retry_num, self.requestId)
  452. def build_write(self):
  453. try:
  454. if self.fps is None or self.width is None or self.height is None:
  455. logger.error("fps、 width、 height为空, requestId:{}", self.requestId)
  456. raise ServiceException(ExceptionType.SERVICE_INNER_EXCEPTION.value[0],
  457. ExceptionType.SERVICE_INNER_EXCEPTION.value[1])
  458. if self.orFilePath is not None and self.or_video_file is None:
  459. self.or_video_file = cv2.VideoWriter(self.orFilePath, cv2.VideoWriter_fourcc(*'mp4v'), self.fps,
  460. (int(self.wn), int(self.hn)))
  461. if self.or_video_file is None:
  462. logger.error("or_video_file为空, requestId:{}", self.requestId)
  463. raise ServiceException(ExceptionType.SERVICE_INNER_EXCEPTION.value[0],
  464. ExceptionType.SERVICE_INNER_EXCEPTION.value[1])
  465. if self.aiFilePath is not None and self.ai_video_file is None:
  466. self.ai_video_file = cv2.VideoWriter(self.aiFilePath, cv2.VideoWriter_fourcc(*'mp4v'), self.fps,
  467. (int(self.wn * 2), int(self.hn)))
  468. if self.ai_video_file is None:
  469. logger.error("ai_video_file为空, requestId:{}", self.requestId)
  470. raise ServiceException(ExceptionType.SERVICE_INNER_EXCEPTION.value[0],
  471. ExceptionType.SERVICE_INNER_EXCEPTION.value[1])
  472. except ServiceException as s:
  473. logger.exception("构建文件写对象异常: {}, requestId:{}", s, self.requestId)
  474. raise s
  475. except Exception as e:
  476. logger.exception("构建文件写对象异常: {}, requestId:{}", e, self.requestId)
  477. raise e
  478. def video_merge(self, frame1, frame2):
  479. # frameLeft = cv2.resize(frame1, (int(self.width / 2), int(self.height / 2)), interpolation=cv2.INTER_LINEAR)
  480. # frameRight = cv2.resize(frame2, (int(self.width / 2), int(self.height / 2)), interpolation=cv2.INTER_LINEAR)
  481. # frame_merge = np.hstack((frameLeft, frameRight))
  482. frame_merge = np.hstack((frame1, frame2))
  483. return frame_merge
  484. def getP(self):
  485. if self.p is None:
  486. logger.error("获取管道为空, requestId:{}", self.requestId)
  487. raise ServiceException(ExceptionType.SERVICE_INNER_EXCEPTION.value[0],
  488. ExceptionType.SERVICE_INNER_EXCEPTION.value[1])
  489. return self.p
  490. def getOrVideoFile(self):
  491. if self.or_video_file is None:
  492. logger.error("获取原视频写入对象为空, requestId:{}", self.requestId)
  493. raise ServiceException(ExceptionType.SERVICE_INNER_EXCEPTION.value[0],
  494. ExceptionType.SERVICE_INNER_EXCEPTION.value[1])
  495. return self.or_video_file
  496. def getAiVideoFile(self):
  497. if self.ai_video_file is None:
  498. logger.error("获取AI视频写入对象为空, requestId:{}", self.requestId)
  499. raise ServiceException(ExceptionType.SERVICE_INNER_EXCEPTION.value[0],
  500. ExceptionType.SERVICE_INNER_EXCEPTION.value[1])
  501. return self.ai_video_file