ffmpeg.py 17 KB

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