ffmpeg.py 19 KB

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