ffmpeg.py 14 KB

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