ffmpeg.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. import asyncio
  2. import json
  3. import os
  4. import subprocess
  5. import time
  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. if total_duration == 0:
  94. return gg_duration_url
  95. duration = int(total_duration) - int(gg_duration_total)
  96. if int(total_duration) < int(gg_duration_total):
  97. return gg_duration_url
  98. cls.asyncio_run_subprocess([
  99. "ffmpeg",
  100. "-i", video_path,
  101. "-c:v", "libx264",
  102. "-c:a", "aac",
  103. "-t", str(duration),
  104. "-y",
  105. gg_duration_url
  106. ], timeout= 360)
  107. return gg_duration_url
  108. except Exception as e:
  109. return gg_duration_url
  110. """
  111. 截取原视频最后一帧
  112. """
  113. @classmethod
  114. def video_png(cls, video_path, file_path):
  115. # 获取视频的原始宽高信息
  116. jpg_url = file_path + 'png.jpg'
  117. try:
  118. cls.asyncio_run_subprocess(
  119. ["ffmpeg", "-sseof", "-1", '-i', video_path, '-frames:v', '1', "-y", jpg_url], timeout=120)
  120. return jpg_url
  121. except Exception as e:
  122. return jpg_url
  123. """
  124. 获取视频音频
  125. """
  126. @classmethod
  127. def get_video_mp3(cls, video_file, video_path_url, pw_random_id):
  128. pw_mp3_path = video_path_url + str(pw_random_id) +'pw_video.mp3'
  129. try:
  130. cls.asyncio_run_subprocess([
  131. 'ffmpeg',
  132. '-i', video_file,
  133. '-q:a', '0',
  134. '-map', 'a',
  135. pw_mp3_path
  136. ], timeout=120)
  137. time.sleep(1)
  138. return pw_mp3_path
  139. except Exception as e:
  140. return pw_mp3_path
  141. """横屏视频改为竖屏"""
  142. @classmethod
  143. def update_video_h_w(cls, video_path, file_path):
  144. video_h_w_path = file_path +'video_h_w_video.mp4'
  145. try:
  146. 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)
  147. return video_h_w_path
  148. except Exception as e:
  149. return video_h_w_path
  150. """视频转为640像素"""
  151. @classmethod
  152. def video_640(cls, video_path, file_path):
  153. video_url = file_path + 'pixelvideo.mp4'
  154. try:
  155. cls.asyncio_run_subprocess(["ffmpeg" ,"-i" ,video_path ,"-vf" ,"scale=360:640" ,video_url],timeout=420)
  156. return video_url
  157. except Exception as e:
  158. return video_url
  159. @classmethod
  160. def concatenate_videos(cls, videos_paths, file_path):
  161. video_url = file_path + 'rg_pw.mp4'
  162. list_filename = file_path + 'rg_pw.txt'
  163. with open(list_filename, "w") as f:
  164. for video_path in videos_paths:
  165. f.write(f"file '{video_path}'\n")
  166. try:
  167. cls.asyncio_run_subprocess(
  168. ["ffmpeg", "-f", "concat", "-safe", "0", "-i", list_filename, "-c", "copy", video_url], timeout=420)
  169. logger.info(f"[+] 视频转为640像素成功")
  170. return video_url
  171. except Exception as e:
  172. return video_url
  173. """视频拼接到一起"""
  174. @classmethod
  175. def h_b_video(cls, video_path, pw_path, file_path):
  176. video_url = file_path + 'hbvideo.mp4'
  177. try:
  178. 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)
  179. return video_url
  180. except Exception as e:
  181. return video_url
  182. """横屏视频顶部增加字幕"""
  183. @classmethod
  184. def add_video_zm(cls, new_video_path, video_path_url, pw_random_id, new_text):
  185. single_video_srt = video_path_url + str(pw_random_id) +'video_zm.srt'
  186. single_video_txt = video_path_url + str(pw_random_id) +'video_zm.txt'
  187. single_video = video_path_url + str(pw_random_id) +'video_zm.mp4'
  188. try:
  189. duration = cls.get_video_duration(new_video_path)
  190. if duration == 0:
  191. return new_video_path
  192. start_time = cls.seconds_to_srt_time(0)
  193. end_time = cls.seconds_to_srt_time(duration)
  194. # zm = '致敬伟大的教员,为整个民族\n感谢老人家历史向一代伟人'
  195. with open(single_video_txt, 'w') as f:
  196. f.write(f"file '{new_video_path}'\n")
  197. with open(single_video_srt, 'w') as f:
  198. f.write(f"1\n{start_time} --> {end_time}\n{new_text}\n\n")
  199. subtitle_cmd = f"subtitles={single_video_srt}:force_style='Fontsize=12,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=225'"
  200. draw = f"{subtitle_cmd}"
  201. cls.asyncio_run_subprocess([
  202. "ffmpeg",
  203. "-f", "concat",
  204. "-safe", "0",
  205. "-i", single_video_txt,
  206. "-c:v", "libx264",
  207. "-c:a", "aac",
  208. "-vf", draw,
  209. "-y",
  210. single_video
  211. ],timeout=500)
  212. # subprocess.run(ffmpeg_cmd)
  213. return single_video
  214. except Exception as e:
  215. return single_video
  216. """获取mp3时长"""
  217. @classmethod
  218. def get_mp3_duration(cls, file_path):
  219. audio = MP3(file_path)
  220. duration = audio.info.length
  221. if duration:
  222. return int(duration)
  223. return 0
  224. """
  225. 生成片尾视频
  226. """
  227. @classmethod
  228. def pw_video(cls, jpg_path, file_path, pw_mp3_path, pw_srt):
  229. # 添加音频到图片
  230. """
  231. jpg_url 图片地址
  232. pw_video 提供的片尾视频
  233. pw_duration 提供的片尾视频时长
  234. new_video_path 视频位置
  235. subtitle_cmd 字幕
  236. pw_url 生成视频地址
  237. :return:
  238. """
  239. pw_srt_path = file_path +'pw_video.srt'
  240. with open(pw_srt_path, 'w') as f:
  241. f.write(pw_srt)
  242. pw_url_path = file_path + 'pw_video.mp4'
  243. try:
  244. pw_duration = cls.get_mp3_duration(pw_mp3_path)
  245. if pw_duration == 0:
  246. return pw_url_path
  247. time.sleep(2)
  248. # 添加字幕 wqy-zenhei Hiragino Sans GB
  249. height = 1080
  250. margin_v = int(height) // 8 # 可根据需要调整字幕和背景之间的距离
  251. 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}'"
  252. bg_position_offset = (int(360) - 360//8) / 1.75
  253. background_cmd = f"drawbox=y=(ih-{int(360)}/2-{bg_position_offset}):color=yellow@1.0:width=iw:height={int(360)}/4:t=fill"
  254. if "mp4" in jpg_path:
  255. pw_path_txt = file_path + 'pw_path_video.txt'
  256. with open(pw_path_txt, 'w') as f:
  257. f.write(f"file '{jpg_path}'\n")
  258. cls.asyncio_run_subprocess([
  259. "ffmpeg",
  260. "-f", "concat",
  261. "-safe", "0",
  262. "-i", f"{pw_path_txt}", # 视频序列输入的文本文件
  263. "-i", pw_mp3_path, # 音频文件
  264. "-c:v", "libx264", # 视频编码格式
  265. "-t", str(pw_duration), # 输出视频的持续时间
  266. "-c:a", "aac", # 音频编码格式
  267. "-b:v", "260k", # 视频比特率
  268. "-b:a", "96k", # 音频比特率
  269. "-threads", "2", # 线程数
  270. "-vf", f"{background_cmd},{subtitle_cmd}", # 视频过滤器(背景和字幕)
  271. "-map", "0:v:0", # 映射视频流来自第一个输入文件(视频)
  272. "-map", "1:a:0", # 映射音频流来自第二个输入文件(音频)
  273. "-y", # 强制覆盖输出文件
  274. pw_url_path # 输出文件路径
  275. ], timeout=500)
  276. else:
  277. cls.asyncio_run_subprocess([
  278. 'ffmpeg',
  279. '-loop', '1',
  280. '-i', jpg_path, # 输入的图片文件
  281. '-i', pw_mp3_path, # 输入的音频文件
  282. '-c:v', 'libx264', # 视频编码格式
  283. '-t', str(pw_duration), # 输出视频的持续时间,与音频持续时间相同
  284. '-pix_fmt', 'yuv420p', # 像素格式
  285. '-c:a', 'aac', # 音频编码格式
  286. '-strict', 'experimental', # 使用实验性编码器
  287. '-shortest', # 确保输出视频的长度与音频一致
  288. '-vf', f"{background_cmd},{subtitle_cmd}", # 视频过滤器,设置分辨率和其他过滤器
  289. pw_url_path # 输出的视频文件路径
  290. ], timeout=500)
  291. if os.path.exists(pw_srt_path):
  292. os.remove(pw_srt_path)
  293. return pw_url_path
  294. except Exception as e:
  295. return pw_url_path
  296. """
  297. 单个视频拼接
  298. """
  299. @classmethod
  300. def single_video(cls, video_path, file_path, zm):
  301. single_video_url = file_path + 'single_video.mp4'
  302. single_video_srt = file_path + 'single_video.srt'
  303. # 获取时长
  304. try:
  305. duration = cls.get_video_duration(video_path)
  306. if duration == 0:
  307. return single_video_url
  308. start_time = cls.seconds_to_srt_time(2)
  309. end_time = cls.seconds_to_srt_time(duration)
  310. single_video_txt = file_path + 'single_video.txt'
  311. with open(single_video_txt, 'w') as f:
  312. f.write(f"file '{video_path}'\n")
  313. if zm:
  314. with open(single_video_srt, 'w') as f:
  315. f.write(f"1\n{start_time} --> {end_time}\n<font color=\"red\">\u2764\uFE0F</font>{zm}\n\n")
  316. subtitle_cmd = f"subtitles={single_video_srt}:force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=20'"
  317. else:
  318. subtitle_cmd = f"force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=20'"
  319. # 多线程数
  320. num_threads = 5
  321. # 构建 FFmpeg 命令,生成视频
  322. cls.asyncio_run_subprocess([
  323. "ffmpeg",
  324. "-f", "concat",
  325. "-safe", "0",
  326. "-i", f"{single_video_txt}",
  327. "-c:v", "libx264",
  328. "-c:a", "aac",
  329. '-b:v', '260k',
  330. "-b:a", "96k",
  331. "-threads", str(num_threads),
  332. "-vf", subtitle_cmd,
  333. "-y",
  334. single_video_url
  335. ], timeout=400)
  336. if os.path.exists(single_video_srt):
  337. os.remove(single_video_srt)
  338. return single_video_url
  339. except Exception as e:
  340. return single_video_url
  341. @classmethod
  342. def asyncio_run_subprocess(cls, params: List[str], timeout: int = 30) -> str:
  343. async def run_subprocess():
  344. process = await asyncio.create_subprocess_exec(
  345. params[0],
  346. *params[1:],
  347. stdout=asyncio.subprocess.PIPE,
  348. stderr=asyncio.subprocess.PIPE,
  349. )
  350. try:
  351. out, err = await asyncio.wait_for(process.communicate(), timeout=timeout)
  352. if process.returncode != 0:
  353. raise IOError(err)
  354. return out.decode()
  355. except asyncio.TimeoutError:
  356. process.kill()
  357. out, err = await process.communicate()
  358. raise IOError(err)
  359. return asyncio.run(run_subprocess())
  360. @classmethod
  361. def get_http_duration(cls, videos_path):
  362. total_duration = 0
  363. for video_path in videos_path:
  364. url = "http://61.48.133.26:5555/api/v1/ffmpeg/get_meta"
  365. payload = json.dumps({
  366. "url": video_path,
  367. "referer": ""
  368. })
  369. headers = {
  370. 'Authorization': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNGNhMTI4ZGYtYWMzMy00NWQ2LTg3MmEtMDAzOTk4MGVhM2ViIiwibmFtZSI6Inp5IiwiZXhwIjoyMDUwOTI3MjExfQ.k_rvuESjA62RgPDiLniVgJyLJn3Q8C1Y_AGq3CPRuKI',
  371. 'Content-Type': 'application/json'
  372. }
  373. try:
  374. response = requests.request("POST", url, headers=headers, data=payload, timeout=30)
  375. response = response.json()
  376. duration = response['data']['streams'][0]['duration']
  377. total_duration += int(float(duration))
  378. except Exception as e:
  379. print(f"Error processing {video_path}: {e}")
  380. return total_duration
  381. if __name__ == '__main__':
  382. file_path = '/Users/z/Downloads/478de0b6-4e52-44a5-a5d4-967b2cf8ce49'
  383. jpg_path = '/Users/z/Downloads/478de0b6-4e52-44a5-a5d4-967b2cf8ce49rg_pixelvideo.mp4'
  384. mp3_path='/Users/z/Downloads/478de0b6-4e52-44a5-a5d4-967b2cf8ce49pw_video.mp3'
  385. pw_srt = """1
  386. 00:00:00,000 --> 00:00:02,842
  387. 这个视频揭示了中国近代历史上
  388. 2
  389. 00:00:02,842 --> 00:00:05,685
  390. 一个鲜为人知却又极为重要的故
  391. 3
  392. 00:00:05,685 --> 00:00:05,888
  393. 4
  394. 00:00:05,888 --> 00:00:07,106
  395. 真是让人震惊
  396. 5
  397. 00:00:07,106 --> 00:00:07,715
  398. 看完后
  399. 6
  400. 00:00:07,715 --> 00:00:10,354
  401. 我不禁对历史有了更深的思考
  402. 7
  403. 00:00:10,354 --> 00:00:12,588
  404. 让我们一起重温这段历史
  405. 8
  406. 00:00:12,588 --> 00:00:14,212
  407. 提醒自己珍惜当下
  408. 9
  409. 00:00:14,212 --> 00:00:17,055
  410. 我相信很多朋友也会对这个话题
  411. 10
  412. 00:00:17,055 --> 00:00:17,664
  413. 感兴趣
  414. 11
  415. 00:00:17,664 --> 00:00:20,506
  416. 请把这个视频分享到你们的群聊
  417. 12
  418. 00:00:20,506 --> 00:00:20,709
  419. 13
  420. 00:00:20,709 --> 00:00:22,740
  421. 让更多人了解这段历史
  422. 14
  423. 00:00:22,820 --> 00:00:23,824
  424. 共鸣与反思
  425. 15
  426. 00:00:23,824 --> 00:00:25,430
  427. 是我们共同的责任
  428. 16
  429. 00:00:25,430 --> 00:00:28,242
  430. 也许我们能从中汲取更多的智慧
  431. 17
  432. 00:00:28,242 --> 00:00:28,844
  433. 与力量
  434. 18
  435. 00:00:28,844 --> 00:00:29,848
  436. 快动动手指
  437. 19
  438. 00:00:29,848 --> 00:00:32,659
  439. 让我们一起分享这段重要的历史
  440. 20
  441. 00:00:32,659 --> 00:00:32,860
  442. 吧"""
  443. FFmpeg.pw_video(jpg_path, file_path, mp3_path, pw_srt)