ffmpeg.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. import asyncio
  2. import json
  3. import os
  4. import time
  5. import traceback
  6. from typing import List
  7. import cv2
  8. import requests
  9. from loguru import logger
  10. from mutagen.mp3 import MP3
  11. class FFmpeg():
  12. """
  13. 时间转换
  14. """
  15. @classmethod
  16. def seconds_to_srt_time(cls, seconds):
  17. hours = int(seconds // 3600)
  18. minutes = int((seconds % 3600) // 60)
  19. seconds = seconds % 60
  20. milliseconds = int((seconds - int(seconds)) * 1000)
  21. return f"{hours:02d}:{minutes:02d}:{int(seconds):02d},{milliseconds:03d}"
  22. """
  23. 获取单个视频时长
  24. """
  25. @classmethod
  26. def get_video_duration(cls, video_url):
  27. cap = cv2.VideoCapture(video_url)
  28. if cap.isOpened():
  29. rate = cap.get(5)
  30. frame_num = cap.get(7)
  31. duration = int(frame_num / rate)
  32. return duration
  33. return 0
  34. # """
  35. # 获取视频文件的时长(秒)
  36. # """
  37. # @classmethod
  38. # def get_videos_duration(cls, video_file):
  39. # result = cls.asyncio_run_subprocess(["ffprobe", "-v", "error", "-show_entries", "format=duration",
  40. # "-of", "default=noprint_wrappers=1:nokey=1", video_file], timeout=10)
  41. # return float(result)
  42. """
  43. 获取视频宽高
  44. """
  45. @classmethod
  46. def get_w_h_size(cls, new_video_path):
  47. try:
  48. # 获取视频的原始宽高信息
  49. ffprobe_cmd = cls.asyncio_run_subprocess(["ffprobe", "-v" ,"error" ,"-select_streams" ,"v:0" ,"-show_entries", "stream=width,height" ,"-of" ,"csv=p=0" ,new_video_path],timeout=10)
  50. output_decoded = ffprobe_cmd.strip()
  51. split_output = [value for value in output_decoded.split(',') if value.strip()]
  52. height, width = map(int, split_output)
  53. return width, height
  54. except ValueError as e:
  55. return 1920, 1080
  56. """
  57. 视频裁剪
  58. """
  59. @classmethod
  60. def video_crop(cls, video_path, file_path):
  61. crop_url = file_path + 'crop.mp4'
  62. try:
  63. # 获取视频的原始宽高信息
  64. ffprobe_cmd = cls.asyncio_run_subprocess(
  65. ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of",
  66. "csv=p=0", video_path], timeout=10)
  67. width, height = map(int, ffprobe_cmd.strip().split(','))
  68. # 计算裁剪后的高度
  69. new_height = int(height * 0.8)
  70. # 构建 FFmpeg 命令,裁剪视频高度为原始高度的80%
  71. cls.asyncio_run_subprocess(
  72. [
  73. "ffmpeg",
  74. "-i", video_path,
  75. "-vf", f"crop={width}:{new_height}",
  76. "-c:v", "libx264",
  77. "-c:a", "aac",
  78. "-y",
  79. crop_url
  80. ],timeout=240)
  81. return crop_url
  82. except Exception as e:
  83. return crop_url
  84. """
  85. 视频截断
  86. """
  87. @classmethod
  88. def video_ggduration(cls, video_path, file_path, gg_duration_total):
  89. gg_duration_url = file_path + 'gg_duration.mp4'
  90. # 获取视频时长
  91. try:
  92. total_duration = cls.get_video_duration(video_path)
  93. logger.info(f"[+]视频总时长{total_duration}")
  94. if total_duration == 0:
  95. return video_path
  96. duration = int(total_duration) - int(gg_duration_total)
  97. if int(total_duration) < int(gg_duration_total):
  98. return video_path
  99. cls.asyncio_run_subprocess([
  100. "ffmpeg",
  101. "-i", video_path,
  102. "-c:v", "libx264",
  103. "-c:a", "aac",
  104. "-t", str(duration),
  105. "-y",
  106. gg_duration_url
  107. ], timeout= 360)
  108. return gg_duration_url
  109. except Exception as e:
  110. logger.error(f"修改视频时长发生异常:{e}\n{traceback.format_exc()}")
  111. return gg_duration_url
  112. """
  113. 截取原视频最后一帧
  114. """
  115. @classmethod
  116. def video_png(cls, video_path, file_path):
  117. # 获取视频的原始宽高信息
  118. jpg_url = file_path + 'png.jpg'
  119. try:
  120. cls.asyncio_run_subprocess(
  121. ["ffmpeg", "-sseof", "-1", '-i', video_path, '-frames:v', '1', "-y", jpg_url], timeout=120)
  122. return jpg_url
  123. except Exception as e:
  124. logger.error(f"[处理] 数据片尾获取最后一帧失败,{e}")
  125. return jpg_url
  126. """
  127. 获取视频音频
  128. """
  129. @classmethod
  130. def get_video_mp3(cls, video_file, video_path_url, pw_random_id):
  131. pw_mp3_path = video_path_url + str(pw_random_id) +'pw_video.mp3'
  132. try:
  133. cls.asyncio_run_subprocess([
  134. 'ffmpeg',
  135. '-i', video_file,
  136. '-q:a', '0',
  137. '-map', 'a',
  138. pw_mp3_path
  139. ], timeout=120)
  140. time.sleep(1)
  141. return pw_mp3_path
  142. except Exception as e:
  143. return pw_mp3_path
  144. @classmethod
  145. def get_pw_video_mp3(cls, video_file, video_path_url):
  146. bgm_pw_path_mp3 = video_file + 'bgm_pw.mp3'
  147. try:
  148. cls.asyncio_run_subprocess([
  149. 'ffmpeg',
  150. '-i', video_path_url, # 输入的视频文件路径
  151. '-q:a', '0', # 设置音频质量为最佳
  152. '-map', 'a',
  153. '-y',
  154. bgm_pw_path_mp3
  155. ], timeout=120)
  156. time.sleep(1)
  157. return bgm_pw_path_mp3
  158. except Exception as e:
  159. return bgm_pw_path_mp3
  160. """
  161. 片尾增加bgm
  162. """
  163. @classmethod
  164. def video_add_bgm(cls, video_file, bgm_path, video_path_url):
  165. bgm_pw_path = video_path_url + 'bgm_pw.mp4'
  166. pw_duration = cls.get_video_duration(video_file)
  167. try:
  168. pw_path_txt = video_path_url + 'bgm_pw_video.txt'
  169. with open(pw_path_txt, 'w') as f:
  170. f.write(f"file '{video_file}'\n")
  171. cls.asyncio_run_subprocess([
  172. "ffmpeg",
  173. "-f", "concat",
  174. "-safe", "0",
  175. "-i", f"{pw_path_txt}", # 视频序列输入的文本文件
  176. "-i", bgm_path, # 音频文件
  177. "-c:v", "libx264", # 视频编码格式
  178. "-t", str(pw_duration), # 输出视频的持续时间
  179. '-c:a', 'aac', # 音频编码格式
  180. '-b:v', '260k', # 视频比特率
  181. '-b:a', '96k', # 音频比特率
  182. '-threads', '2', # 线程数
  183. # '-vf', f'{background_cmd},{subtitle_cmd}', # 视频过滤器(背景和字幕)
  184. "-filter_complex", "[1:a]volume=0.6[a1];[0:a][a1]amerge=inputs=2[aout]", # 混合音频流
  185. "-map", "0:v:0", # 映射视频流来自第一个输入文件(视频)
  186. "-map", "[aout]", # 映射混合后的音频流
  187. '-y', # 强制覆盖输出文件
  188. bgm_pw_path # 输出文件路径
  189. ], timeout=500)
  190. time.sleep(1)
  191. return bgm_pw_path
  192. except Exception as e:
  193. return bgm_pw_path
  194. """横屏视频改为竖屏"""
  195. @classmethod
  196. def update_video_h_w(cls, video_path, file_path):
  197. video_h_w_path = file_path +'video_h_w_video.mp4'
  198. try:
  199. cls.asyncio_run_subprocess(["ffmpeg" ,"-i" ,video_path ,"-vf" ,"scale=640:ih*640/iw,pad=iw:iw*16/9:(ow-iw)/2:(oh-ih)/2" ,video_h_w_path],timeout=420)
  200. return video_h_w_path
  201. except Exception as e:
  202. return video_h_w_path
  203. """视频转为640像素"""
  204. @classmethod
  205. def video_640(cls, video_path, file_path):
  206. video_url = file_path + 'pixelvideo.mp4'
  207. try:
  208. cls.asyncio_run_subprocess(["ffmpeg" ,"-i" ,video_path ,"-vf" ,"scale=360:640" ,video_url],timeout=420)
  209. return video_url
  210. except Exception as e:
  211. return video_url
  212. @classmethod
  213. def concatenate_videos(cls, videos_paths, file_path):
  214. video_url = file_path + 'rg_pw.mp4'
  215. list_filename = file_path + 'rg_pw.txt'
  216. with open(list_filename, "w") as f:
  217. for video_path in videos_paths:
  218. f.write(f"file '{video_path}'\n")
  219. try:
  220. cls.asyncio_run_subprocess(
  221. ["ffmpeg", "-f", "concat", "-safe", "0", "-i", list_filename, "-c", "copy", video_url], timeout=420)
  222. logger.info(f"[+] 视频转为640像素成功")
  223. return video_url
  224. except Exception as e:
  225. return video_url
  226. """视频拼接到一起"""
  227. @classmethod
  228. def h_b_video(cls, video_path, pw_path, file_path):
  229. video_url = file_path + 'hbvideo.mp4'
  230. try:
  231. cls.asyncio_run_subprocess(["ffmpeg","-i", video_path, "-i", pw_path, "-filter_complex" ,"[0:v]scale=360:640[v1]; [1:v]scale=360:640[v2]; [v1][0:a][v2][1:a]concat=n=2:v=1:a=1[outv][outa]" ,"-map" ,"[outv]" ,"-map" ,"[outa]" ,video_url],timeout=500)
  232. return video_url
  233. except Exception as e:
  234. return video_url
  235. """横屏视频顶部增加字幕"""
  236. @classmethod
  237. def add_video_zm(cls, new_video_path, video_path_url, pw_random_id, new_text):
  238. single_video_srt = video_path_url + str(pw_random_id) +'video_zm.srt'
  239. single_video_txt = video_path_url + str(pw_random_id) +'video_zm.txt'
  240. single_video = video_path_url + str(pw_random_id) +'video_zm.mp4'
  241. try:
  242. duration = cls.get_video_duration(new_video_path)
  243. if duration == 0:
  244. return new_video_path
  245. start_time = cls.seconds_to_srt_time(0)
  246. end_time = cls.seconds_to_srt_time(duration)
  247. # zm = '致敬伟大的教员,为整个民族\n感谢老人家历史向一代伟人'
  248. with open(single_video_txt, 'w') as f:
  249. f.write(f"file '{new_video_path}'\n")
  250. with open(single_video_srt, 'w') as f:
  251. f.write(f"1\n{start_time} --> {end_time}\n{new_text}\n\n")
  252. subtitle_cmd = f"subtitles={single_video_srt}:force_style='Fontsize=12,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=225'"
  253. draw = f"{subtitle_cmd}"
  254. cls.asyncio_run_subprocess([
  255. "ffmpeg",
  256. "-f", "concat",
  257. "-safe", "0",
  258. "-i", single_video_txt,
  259. "-c:v", "libx264",
  260. "-c:a", "aac",
  261. "-vf", draw,
  262. "-y",
  263. single_video
  264. ],timeout=500)
  265. # subprocess.run(ffmpeg_cmd)
  266. return single_video
  267. except Exception as e:
  268. return single_video
  269. """获取mp3时长"""
  270. @classmethod
  271. def get_mp3_duration(cls, file_path):
  272. audio = MP3(file_path)
  273. duration = audio.info.length
  274. if duration:
  275. return int(duration)
  276. return 0
  277. """
  278. 生成片尾视频
  279. """
  280. @classmethod
  281. def pw_video(cls, jpg_path, file_path, pw_mp3_path, pw_srt):
  282. # 添加音频到图片
  283. """
  284. jpg_url 图片地址
  285. pw_video 提供的片尾视频
  286. pw_duration 提供的片尾视频时长
  287. new_video_path 视频位置
  288. subtitle_cmd 字幕
  289. pw_url 生成视频地址
  290. :return:
  291. """
  292. pw_srt_path = file_path +'pw_video.srt'
  293. with open(pw_srt_path, 'w') as f:
  294. f.write(pw_srt)
  295. pw_url_path = file_path + 'pw_video.mp4'
  296. try:
  297. pw_duration = cls.get_mp3_duration(pw_mp3_path)
  298. if pw_duration == 0:
  299. return pw_url_path
  300. time.sleep(2)
  301. # 添加字幕 wqy-zenhei Hiragino Sans GB
  302. height = 1080
  303. margin_v = int(height) // 8 # 可根据需要调整字幕和背景之间的距离
  304. subtitle_cmd = f"subtitles={pw_srt_path}:force_style='Fontsize=13,Fontname=wqy-zenhei,Outline=0,PrimaryColour=&H000000,SecondaryColour=&H000000,Bold=1,MarginV={margin_v}'"
  305. bg_position_offset = (int(360) - 360//8) / 1.75
  306. background_cmd = f"drawbox=y=(ih-{int(360)}/2-{bg_position_offset}):color=yellow@1.0:width=iw:height={int(360)}/4:t=fill"
  307. if "mp4" in jpg_path:
  308. pw_path_txt = file_path + 'pw_path_video.txt'
  309. with open(pw_path_txt, 'w') as f:
  310. f.write(f"file '{jpg_path}'\n")
  311. cls.asyncio_run_subprocess([
  312. "ffmpeg",
  313. "-f", "concat",
  314. "-safe", "0",
  315. "-i", f"{pw_path_txt}", # 视频序列输入的文本文件
  316. "-i", pw_mp3_path, # 音频文件
  317. "-c:v", "libx264", # 视频编码格式
  318. "-t", str(pw_duration), # 输出视频的持续时间
  319. "-c:a", "aac", # 音频编码格式
  320. "-b:v", "260k", # 视频比特率
  321. "-b:a", "96k", # 音频比特率
  322. "-threads", "2", # 线程数
  323. "-vf", f"{background_cmd},{subtitle_cmd}", # 视频过滤器(背景和字幕)
  324. "-map", "0:v:0", # 映射视频流来自第一个输入文件(视频)
  325. "-map", "1:a:0", # 映射音频流来自第二个输入文件(音频)
  326. "-y", # 强制覆盖输出文件
  327. pw_url_path # 输出文件路径
  328. ], timeout=500)
  329. else:
  330. cls.asyncio_run_subprocess([
  331. 'ffmpeg',
  332. '-loop', '1',
  333. '-i', jpg_path, # 输入的图片文件
  334. '-i', pw_mp3_path, # 输入的音频文件
  335. '-c:v', 'libx264', # 视频编码格式
  336. '-t', str(pw_duration), # 输出视频的持续时间,与音频持续时间相同
  337. '-pix_fmt', 'yuv420p', # 像素格式
  338. '-c:a', 'aac', # 音频编码格式
  339. '-strict', 'experimental', # 使用实验性编码器
  340. '-shortest', # 确保输出视频的长度与音频一致
  341. '-vf', f"{background_cmd},{subtitle_cmd}", # 视频过滤器,设置分辨率和其他过滤器
  342. pw_url_path # 输出的视频文件路径
  343. ], timeout=500)
  344. if os.path.exists(pw_srt_path):
  345. os.remove(pw_srt_path)
  346. return pw_url_path
  347. except Exception as e:
  348. return pw_url_path
  349. """
  350. 单个视频拼接
  351. """
  352. @classmethod
  353. def single_video(cls, video_path, file_path, zm):
  354. single_video_url = file_path + 'single_video.mp4'
  355. single_video_srt = file_path + 'single_video.srt'
  356. # 获取时长
  357. try:
  358. duration = cls.get_video_duration(video_path)
  359. if duration == 0:
  360. return single_video_url
  361. start_time = cls.seconds_to_srt_time(2)
  362. end_time = cls.seconds_to_srt_time(duration)
  363. single_video_txt = file_path + 'single_video.txt'
  364. with open(single_video_txt, 'w') as f:
  365. f.write(f"file '{video_path}'\n")
  366. if zm:
  367. with open(single_video_srt, 'w') as f:
  368. f.write(f"1\n{start_time} --> {end_time}\n<font color=\"red\">\u2764\uFE0F</font>{zm}\n\n")
  369. subtitle_cmd = f"subtitles={single_video_srt}:force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=20'"
  370. else:
  371. subtitle_cmd = f"force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=20'"
  372. # 多线程数
  373. num_threads = 5
  374. # 构建 FFmpeg 命令,生成视频
  375. cls.asyncio_run_subprocess([
  376. "ffmpeg",
  377. "-f", "concat",
  378. "-safe", "0",
  379. "-i", f"{single_video_txt}",
  380. "-c:v", "libx264",
  381. "-c:a", "aac",
  382. '-b:v', '260k',
  383. "-b:a", "96k",
  384. "-threads", str(num_threads),
  385. "-vf", subtitle_cmd,
  386. "-y",
  387. single_video_url
  388. ], timeout=400)
  389. if os.path.exists(single_video_srt):
  390. os.remove(single_video_srt)
  391. return single_video_url
  392. except Exception as e:
  393. return single_video_url
  394. @classmethod
  395. def asyncio_run_subprocess(cls, params: List[str], timeout: int = 30) -> str:
  396. async def run_subprocess():
  397. process = await asyncio.create_subprocess_exec(
  398. params[0],
  399. *params[1:],
  400. stdout=asyncio.subprocess.PIPE,
  401. stderr=asyncio.subprocess.PIPE,
  402. )
  403. try:
  404. out, err = await asyncio.wait_for(process.communicate(), timeout=timeout)
  405. if process.returncode != 0:
  406. raise IOError(err)
  407. return out.decode()
  408. except asyncio.TimeoutError:
  409. process.kill()
  410. out, err = await process.communicate()
  411. raise IOError(err)
  412. return asyncio.run(run_subprocess())
  413. @classmethod
  414. def get_http_duration(cls, videos_path):
  415. total_duration = 0
  416. for video_path in videos_path:
  417. url = "http://101.37.24.17:5555/api/v1/ffmpeg/get_meta"
  418. payload = json.dumps({
  419. "url": video_path,
  420. "referer": ""
  421. })
  422. headers = {
  423. 'Authorization': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNGNhMTI4ZGYtYWMzMy00NWQ2LTg3MmEtMDAzOTk4MGVhM2ViIiwibmFtZSI6Inp5IiwiZXhwIjoyMDUwOTI3MjExfQ.k_rvuESjA62RgPDiLniVgJyLJn3Q8C1Y_AGq3CPRuKI',
  424. 'Content-Type': 'application/json'
  425. }
  426. try:
  427. response = requests.request("POST", url, headers=headers, data=payload, timeout=30)
  428. response = response.json()
  429. duration = response['data']['streams'][0]['duration']
  430. total_duration += int(float(duration))
  431. except Exception as e:
  432. print(f"Error processing {video_path}: {e}")
  433. return total_duration
  434. if __name__ == '__main__':
  435. file_path = '/Users/z/Downloads/478de0b6-4e52-44a5-a5d4-967b2cf8ce49'
  436. jpg_path = '/Users/z/Downloads/478de0b6-4e52-44a5-a5d4-967b2cf8ce49rg_pixelvideo.mp4'
  437. mp3_path='/Users/z/Downloads/478de0b6-4e52-44a5-a5d4-967b2cf8ce49pw_video.mp3'
  438. pw_srt = """1
  439. 00:00:00,000 --> 00:00:02,842
  440. 这个视频揭示了中国近代历史上
  441. 2
  442. 00:00:02,842 --> 00:00:05,685
  443. 一个鲜为人知却又极为重要的故
  444. 3
  445. 00:00:05,685 --> 00:00:05,888
  446. 4
  447. 00:00:05,888 --> 00:00:07,106
  448. 真是让人震惊
  449. 5
  450. 00:00:07,106 --> 00:00:07,715
  451. 看完后
  452. 6
  453. 00:00:07,715 --> 00:00:10,354
  454. 我不禁对历史有了更深的思考
  455. 7
  456. 00:00:10,354 --> 00:00:12,588
  457. 让我们一起重温这段历史
  458. 8
  459. 00:00:12,588 --> 00:00:14,212
  460. 提醒自己珍惜当下
  461. 9
  462. 00:00:14,212 --> 00:00:17,055
  463. 我相信很多朋友也会对这个话题
  464. 10
  465. 00:00:17,055 --> 00:00:17,664
  466. 感兴趣
  467. 11
  468. 00:00:17,664 --> 00:00:20,506
  469. 请把这个视频分享到你们的群聊
  470. 12
  471. 00:00:20,506 --> 00:00:20,709
  472. 13
  473. 00:00:20,709 --> 00:00:22,740
  474. 让更多人了解这段历史
  475. 14
  476. 00:00:22,820 --> 00:00:23,824
  477. 共鸣与反思
  478. 15
  479. 00:00:23,824 --> 00:00:25,430
  480. 是我们共同的责任
  481. 16
  482. 00:00:25,430 --> 00:00:28,242
  483. 也许我们能从中汲取更多的智慧
  484. 17
  485. 00:00:28,242 --> 00:00:28,844
  486. 与力量
  487. 18
  488. 00:00:28,844 --> 00:00:29,848
  489. 快动动手指
  490. 19
  491. 00:00:29,848 --> 00:00:32,659
  492. 让我们一起分享这段重要的历史
  493. 20
  494. 00:00:32,659 --> 00:00:32,860
  495. 吧"""
  496. FFmpeg.pw_video(jpg_path, file_path, mp3_path, pw_srt)