ffmpeg.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. import os
  2. import subprocess
  3. import time
  4. import cv2
  5. from loguru import logger
  6. from mutagen.mp3 import MP3
  7. class FFmpeg():
  8. """
  9. 时间转换
  10. """
  11. @classmethod
  12. def seconds_to_srt_time(cls, seconds):
  13. hours = int(seconds // 3600)
  14. minutes = int((seconds % 3600) // 60)
  15. seconds = seconds % 60
  16. milliseconds = int((seconds - int(seconds)) * 1000)
  17. return f"{hours:02d}:{minutes:02d}:{int(seconds):02d},{milliseconds:03d}"
  18. """
  19. 获取单个视频时长
  20. """
  21. @classmethod
  22. def get_video_duration(cls, video_url):
  23. cap = cv2.VideoCapture(video_url)
  24. if cap.isOpened():
  25. rate = cap.get(5)
  26. frame_num = cap.get(7)
  27. duration = int(frame_num / rate)
  28. return duration
  29. return 0
  30. """
  31. 获取视频文件的时长(秒)
  32. """
  33. @classmethod
  34. def get_videos_duration(cls, video_file):
  35. result = subprocess.run(
  36. ["ffprobe", "-v", "error", "-show_entries", "format=duration",
  37. "-of", "default=noprint_wrappers=1:nokey=1", video_file],
  38. capture_output=True, text=True)
  39. return float(result.stdout)
  40. """
  41. 视频裁剪
  42. """
  43. @classmethod
  44. def video_tailor(cls, video_url):
  45. output_video_path = ''
  46. try:
  47. # 获取视频的原始宽高信息
  48. width, height = cls.get_w_h_size(video_url)
  49. # 计算裁剪后的高度
  50. new_height = int(height * 0.8)
  51. # 构建 FFmpeg 命令,裁剪视频高度为原始高度的70%,并将宽度缩放为320x480
  52. ffmpeg_cmd = [
  53. "ffmpeg",
  54. "-i", video_url,
  55. "-vf", f"crop={width}:{new_height},scale=320:480",
  56. "-c:v", "libx264",
  57. "-c:a", "aac",
  58. "-y",
  59. output_video_path
  60. ]
  61. # 执行 FFmpeg 命令
  62. subprocess.run(ffmpeg_cmd, check=True)
  63. return output_video_path
  64. except Exception as e:
  65. return None
  66. """
  67. 获取视频宽高
  68. """
  69. @classmethod
  70. def get_w_h_size(cls, new_video_path):
  71. try:
  72. # 获取视频的原始宽高信息
  73. ffprobe_cmd = f"ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 {new_video_path}"
  74. ffprobe_process = subprocess.Popen(ffprobe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
  75. output, _ = ffprobe_process.communicate()
  76. output_decoded = output.decode().strip()
  77. split_output = [value for value in output_decoded.split(',') if value.strip()]
  78. height, width = map(int, split_output)
  79. return width, height
  80. except ValueError as e:
  81. return 1920, 1080
  82. """
  83. 视频裁剪
  84. """
  85. @classmethod
  86. def video_crop(cls, video_path, file_path):
  87. crop_url = file_path + 'crop.mp4'
  88. # 获取视频的原始宽高信息
  89. ffprobe_cmd = f"ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 {new_video_path}"
  90. ffprobe_process = subprocess.Popen(ffprobe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
  91. output, _ = ffprobe_process.communicate()
  92. width, height = map(int, output.decode().strip().split(','))
  93. # 计算裁剪后的高度
  94. new_height = int(height * 0.8)
  95. # 构建 FFmpeg 命令,裁剪视频高度为原始高度的80%
  96. ffmpeg_cmd = [
  97. "ffmpeg",
  98. "-i", video_path,
  99. "-vf", f"crop={width}:{new_height}",
  100. "-c:v", "libx264",
  101. "-c:a", "aac",
  102. "-y",
  103. crop_url
  104. ]
  105. subprocess.run(ffmpeg_cmd)
  106. return crop_url
  107. """
  108. 视频截断
  109. """
  110. @classmethod
  111. def video_ggduration(cls, video_path, file_path, gg_duration_total):
  112. gg_duration_url = file_path + 'gg_duration.mp4'
  113. # 获取视频时长
  114. total_duration = cls.get_video_duration(video_path)
  115. if total_duration == 0:
  116. return new_video_path
  117. duration = int(total_duration) - int(gg_duration_total)
  118. if int(total_duration) < int(gg_duration_total):
  119. return new_video_path
  120. ffmpeg_cmd = [
  121. "ffmpeg",
  122. "-i", video_path,
  123. "-c:v", "libx264",
  124. "-c:a", "aac",
  125. "-t", str(duration),
  126. "-y",
  127. gg_duration_url
  128. ]
  129. subprocess.run(ffmpeg_cmd)
  130. return gg_duration_url
  131. """
  132. 截取原视频最后一帧
  133. """
  134. @classmethod
  135. def video_png(cls, video_path, file_path):
  136. """
  137. """
  138. # 获取视频的原始宽高信息
  139. jpg_url = file_path + 'png.jpg'
  140. # 获取视频时长
  141. total_duration = cls.get_video_duration(video_path)
  142. if total_duration == 0:
  143. return jpg_url
  144. time_offset = total_duration - 1 # 提取倒数第一秒的帧
  145. # 获取视频最后一秒,生成.jpg
  146. subprocess.run(
  147. ['ffmpeg', '-ss', str(time_offset), '-i', video_path, '-t', str(total_duration), '-vf', 'fps=1,scale=360:640', "-y", jpg_url])
  148. return jpg_url
  149. """
  150. 获取视频音频
  151. """
  152. @classmethod
  153. def get_video_mp3(cls, video_file, video_path_url, pw_random_id):
  154. pw_mp3_path = video_path_url + str(pw_random_id) +'pw_video.mp3'
  155. command = [
  156. 'ffmpeg',
  157. '-i', video_file,
  158. '-q:a', '0',
  159. '-map', 'a',
  160. # '-codec:a', 'libmp3lame', # 指定 MP3 编码器
  161. pw_mp3_path
  162. ]
  163. subprocess.run(command)
  164. time.sleep(1)
  165. return pw_mp3_path
  166. """横屏视频改为竖屏"""
  167. @classmethod
  168. def update_video_h_w(cls, video_path, file_path):
  169. video_h_w_path = file_path +'video_h_w_video.mp4'
  170. ffmpeg_cmd = f"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}"
  171. subprocess.run(ffmpeg_cmd, shell=True)
  172. return video_h_w_path
  173. """视频转为640像素"""
  174. @classmethod
  175. def video_640(cls, video_path, file_path):
  176. video_url = file_path + 'pixelvideo.mp4'
  177. ffmpeg_cmd = f"ffmpeg -i {video_path} -vf 'scale=360:640' {video_url}"
  178. subprocess.run(ffmpeg_cmd, shell=True)
  179. return video_url
  180. """视频拼接到一起"""
  181. @classmethod
  182. def h_b_video(cls, video_path, pw_path, file_path, zm):
  183. video_url = file_path + 'hbvideo.mp4'
  184. single_video_srt = file_path + 'single_video.srt'
  185. # 获取时长
  186. duration = cls.get_video_duration(video_path)
  187. pw_duration = cls.get_video_duration(pw_path)
  188. if duration == 0 or pw_duration == 0:
  189. return video_url
  190. duration = pw_duration + duration
  191. start_time = cls.seconds_to_srt_time(2)
  192. end_time = cls.seconds_to_srt_time(duration)
  193. single_video_txt = file_path + 'single_video.txt'
  194. with open(single_video_txt, 'w') as f:
  195. f.write(f"file '{video_path}'\n")
  196. if zm:
  197. # 如果有 zm,则写入字幕文件
  198. with open(single_video_srt, 'w') as f:
  199. f.write(f"1\n{start_time} --> {end_time}\n<font color=\"red\">\u2764\uFE0F</font>{zm}\n\n")
  200. # 字幕命令,包含字幕文件路径
  201. subtitle_cmd = f"subtitles={single_video_srt}:force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=20'"
  202. else:
  203. # 如果没有 zm,则不使用字幕文件,直接使用样式
  204. subtitle_cmd = f"force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=20'"
  205. # 最终的ffmpeg命令,加入字幕命令
  206. ffmpeg_cmd = f"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]' {subtitle_cmd} {video_url}"
  207. subprocess.run(ffmpeg_cmd, shell=True)
  208. return video_url
  209. """横屏视频顶部增加字幕"""
  210. @classmethod
  211. def add_video_zm(cls, new_video_path, video_path_url, pw_random_id, new_text):
  212. single_video_srt = video_path_url + str(pw_random_id) +'video_zm.srt'
  213. single_video_txt = video_path_url + str(pw_random_id) +'video_zm.txt'
  214. single_video = video_path_url + str(pw_random_id) +'video_zm.mp4'
  215. duration = cls.get_video_duration(new_video_path)
  216. if duration == 0:
  217. return new_video_path
  218. start_time = cls.seconds_to_srt_time(0)
  219. end_time = cls.seconds_to_srt_time(duration)
  220. # zm = '致敬伟大的教员,为整个民族\n感谢老人家历史向一代伟人'
  221. with open(single_video_txt, 'w') as f:
  222. f.write(f"file '{new_video_path}'\n")
  223. with open(single_video_srt, 'w') as f:
  224. f.write(f"1\n{start_time} --> {end_time}\n{new_text}\n\n")
  225. subtitle_cmd = f"subtitles={single_video_srt}:force_style='Fontsize=12,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=225'"
  226. draw = f"{subtitle_cmd}"
  227. ffmpeg_cmd = [
  228. "ffmpeg",
  229. "-f", "concat",
  230. "-safe", "0",
  231. "-i", single_video_txt,
  232. "-c:v", "libx264",
  233. "-c:a", "aac",
  234. # '-vf', f"scale=640x360",
  235. "-vf", draw,
  236. "-y",
  237. single_video
  238. ]
  239. subprocess.run(ffmpeg_cmd)
  240. return single_video
  241. """获取mp3时长"""
  242. @classmethod
  243. def get_mp3_duration(cls, file_path):
  244. audio = MP3(file_path)
  245. duration = audio.info.length
  246. if duration:
  247. return int(duration)
  248. return 0
  249. """
  250. 生成片尾视频
  251. """
  252. @classmethod
  253. def pw_video(cls, jpg_path, file_path, pw_mp3_path, pw_srt):
  254. # 添加音频到图片
  255. """
  256. jpg_url 图片地址
  257. pw_video 提供的片尾视频
  258. pw_duration 提供的片尾视频时长
  259. new_video_path 视频位置
  260. subtitle_cmd 字幕
  261. pw_url 生成视频地址
  262. :return:
  263. """
  264. pw_srt_path = file_path +'pw_video.srt'
  265. with open(pw_srt_path, 'w') as f:
  266. f.write(pw_srt)
  267. pw_url_path = file_path + 'pw_video.mp4'
  268. pw_duration = cls.get_mp3_duration(pw_mp3_path)
  269. if pw_duration == 0:
  270. return pw_url_path
  271. time.sleep(2)
  272. # 添加字幕 wqy-zenhei Hiragino Sans GB
  273. height = 1080
  274. margin_v = int(height) // 8 # 可根据需要调整字幕和背景之间的距离
  275. 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}'"
  276. bg_position_offset = (int(360) - 360//8) / 1.75
  277. background_cmd = f"drawbox=y=(ih-{int(360)}/2-{bg_position_offset}):color=yellow@1.0:width=iw:height={int(360)}/4:t=fill"
  278. ffmpeg_cmd = [
  279. 'ffmpeg',
  280. '-loop', '1',
  281. '-i', jpg_path, # 输入的图片文件
  282. '-i', pw_mp3_path, # 输入的音频文件
  283. '-c:v', 'libx264', # 视频编码格式
  284. '-t', str(pw_duration), # 输出视频的持续时间,与音频持续时间相同
  285. '-pix_fmt', 'yuv420p', # 像素格式
  286. '-c:a', 'aac', # 音频编码格式
  287. '-strict', 'experimental', # 使用实验性编码器
  288. '-shortest', # 确保输出视频的长度与音频一致
  289. '-vf', f"{background_cmd},{subtitle_cmd}", # 视频过滤器,设置分辨率和其他过滤器
  290. pw_url_path # 输出的视频文件路径
  291. ]
  292. subprocess.run(ffmpeg_cmd)
  293. if os.path.exists(pw_srt_path):
  294. os.remove(pw_srt_path)
  295. return pw_url_path
  296. """
  297. 设置统一格式拼接视频
  298. """
  299. @classmethod
  300. def concatenate_videos(cls, video_list, video_path):
  301. concatenate_videos_url = video_path + 'concatenate_videos.mp4'
  302. # 拼接视频
  303. VIDEO_COUNTER = 0
  304. FF_INPUT = ""
  305. FF_SCALE = ""
  306. FF_FILTER = ""
  307. ffmpeg_cmd = ["ffmpeg"]
  308. for videos in video_list:
  309. # 添加输入文件
  310. FF_INPUT += f" -i {videos}"
  311. # 为每个视频文件统一长宽,并设置SAR(采样宽高比)
  312. FF_SCALE += f"[{VIDEO_COUNTER}:v]scale=360x640,setsar=1[v{VIDEO_COUNTER}];"
  313. # 为每个视频文件创建一个输入流,并添加到-filter_complex参数中
  314. FF_FILTER += f"[v{VIDEO_COUNTER}][{VIDEO_COUNTER}:a]"
  315. # 增加视频计数器
  316. VIDEO_COUNTER += 1
  317. ffmpeg_cmd.extend(FF_INPUT.split())
  318. ffmpeg_cmd.extend(["-filter_complex", f"{FF_SCALE}{FF_FILTER}concat=n={VIDEO_COUNTER}:v=1:a=1[v][a]",
  319. "-map", "[v]", "-map", "[a]", "-y", concatenate_videos_url])
  320. try:
  321. print(ffmpeg_cmd)
  322. # result = subprocess.run(ffmpeg_cmd)
  323. # if result.returncode != 0:
  324. # return None
  325. # else:
  326. # return concatenate_videos_url
  327. except Exception as e:
  328. logger.error(f"[+] 视频合并失败,失败信息{e}")
  329. return None
  330. """
  331. 单个视频拼接
  332. """
  333. @classmethod
  334. def single_video(cls, video_path, file_path, zm):
  335. single_video_url = file_path + 'single_video.mp4'
  336. single_video_srt = file_path + 'single_video.srt'
  337. # 获取时长
  338. duration = cls.get_video_duration(video_path)
  339. if duration == 0:
  340. return single_video_url
  341. start_time = cls.seconds_to_srt_time(2)
  342. end_time = cls.seconds_to_srt_time(duration)
  343. single_video_txt = file_path + 'single_video.txt'
  344. with open(single_video_txt, 'w') as f:
  345. f.write(f"file '{video_path}'\n")
  346. if zm:
  347. with open(single_video_srt, 'w') as f:
  348. f.write(f"1\n{start_time} --> {end_time}\n<font color=\"red\">\u2764\uFE0F</font>{zm}\n\n")
  349. subtitle_cmd = f"subtitles={single_video_srt}:force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=20'"
  350. else:
  351. subtitle_cmd = f"force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=20'"
  352. draw = f"{subtitle_cmd}"
  353. # 多线程数
  354. num_threads = 5
  355. # 构建 FFmpeg 命令,生成视频
  356. ffmpeg_cmd_oss = [
  357. "ffmpeg",
  358. "-f", "concat",
  359. "-safe", "0",
  360. "-i", f"{single_video_txt}",
  361. "-c:v", "libx264",
  362. "-c:a", "aac",
  363. '-b:v', '260k',
  364. "-b:a", "96k",
  365. "-threads", str(num_threads),
  366. "-vf", f"{draw}",
  367. "-y",
  368. single_video_url
  369. ]
  370. subprocess.run(ffmpeg_cmd_oss)
  371. if os.path.exists(single_video_srt):
  372. os.remove(single_video_srt)
  373. return single_video_url
  374. if __name__ == '__main__':
  375. new_video_path = '/Users/tzld/Desktop/video_rewriting/path/output1.mp4'
  376. video_path_url = '/Users/tzld/Desktop/video_rewriting/path/'
  377. zm = '温馨提示:下方按钮可分享到群'
  378. FFmpeg.single_video(new_video_path, video_path_url, zm)