ffmpeg.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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. ffprobe_cmd = [
  20. "ffprobe",
  21. "-i", video_url,
  22. "-show_entries", "format=duration",
  23. "-v", "quiet",
  24. "-of", "csv=p=0"
  25. ]
  26. output = subprocess.check_output(ffprobe_cmd).decode("utf-8").strip()
  27. return float(output)
  28. """
  29. 获取视频文件的时长(秒)
  30. """
  31. @classmethod
  32. def get_videos_duration(cls, video_file):
  33. result = subprocess.run(
  34. ["ffprobe", "-v", "error", "-show_entries", "format=duration",
  35. "-of", "default=noprint_wrappers=1:nokey=1", video_file],
  36. capture_output=True, text=True)
  37. return float(result.stdout)
  38. """
  39. 视频裁剪
  40. """
  41. @classmethod
  42. def video_tailor(cls, video_url):
  43. output_video_path = ''
  44. try:
  45. # 获取视频的原始宽高信息
  46. width, height = cls.get_w_h_size(video_url)
  47. # 计算裁剪后的高度
  48. new_height = int(height * 0.8)
  49. # 构建 FFmpeg 命令,裁剪视频高度为原始高度的70%,并将宽度缩放为320x480
  50. ffmpeg_cmd = [
  51. "ffmpeg",
  52. "-i", video_url,
  53. "-vf", f"crop={width}:{new_height},scale=320:480",
  54. "-c:v", "libx264",
  55. "-c:a", "aac",
  56. "-y",
  57. output_video_path
  58. ]
  59. # 执行 FFmpeg 命令
  60. subprocess.run(ffmpeg_cmd, check=True)
  61. return output_video_path
  62. except Exception as e:
  63. return None
  64. """
  65. 获取视频宽高
  66. """
  67. @classmethod
  68. def get_w_h_size(cls, new_video_path):
  69. # 获取视频的原始宽高信息
  70. ffprobe_cmd = f"ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 {new_video_path}"
  71. ffprobe_process = subprocess.Popen(ffprobe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
  72. output, _ = ffprobe_process.communicate()
  73. height, width = map(int, output.decode().strip().split(','))
  74. return width, height
  75. """
  76. 视频裁剪
  77. """
  78. @classmethod
  79. def video_crop(cls, new_video_path, video_path_url, pw_random_id):
  80. crop_url = video_path_url + str(pw_random_id) + 'crop.mp4'
  81. # 获取视频的原始宽高信息
  82. ffprobe_cmd = f"ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 {new_video_path}"
  83. ffprobe_process = subprocess.Popen(ffprobe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
  84. output, _ = ffprobe_process.communicate()
  85. width, height = map(int, output.decode().strip().split(','))
  86. # 计算裁剪后的高度
  87. new_height = int(height * 0.8)
  88. # 构建 FFmpeg 命令,裁剪视频高度为原始高度的80%
  89. ffmpeg_cmd = [
  90. "ffmpeg",
  91. "-i", new_video_path,
  92. "-vf", f"crop={width}:{new_height}",
  93. "-c:v", "libx264",
  94. "-c:a", "aac",
  95. "-y",
  96. crop_url
  97. ]
  98. subprocess.run(ffmpeg_cmd)
  99. return crop_url
  100. """
  101. 视频裁剪
  102. """
  103. @classmethod
  104. def video_ggduration(cls, new_video_path, video_path_url, pw_random_id, gg_duration_total):
  105. gg_duration_url = video_path_url + str(pw_random_id) + 'gg_duration.mp4'
  106. # 获取视频时长
  107. total_duration = cls.get_video_duration(new_video_path)
  108. duration = int(total_duration) - int(gg_duration_total)
  109. ffmpeg_cmd = [
  110. "ffmpeg",
  111. "-i", new_video_path,
  112. "-c:v", "libx264",
  113. "-c:a", "aac",
  114. "-t", str(duration),
  115. "-y",
  116. gg_duration_url
  117. ]
  118. subprocess.run(ffmpeg_cmd)
  119. return gg_duration_url
  120. """
  121. 截取原视频最后一帧
  122. """
  123. @classmethod
  124. def video_png(cls, new_video_path, video_path_url, pw_random_id):
  125. """
  126. jpg_url 生成图片位置
  127. :param new_video_path: 视频地址
  128. :return:
  129. """
  130. # 获取视频的原始宽高信息
  131. width, height = cls.get_w_h_size(new_video_path)
  132. jpg_url = video_path_url + str(pw_random_id) + 'png.jpg'
  133. # 获取视频时长
  134. total_duration = cls.get_video_duration(new_video_path)
  135. time_offset = total_duration - 1 # 提取倒数第一秒的帧
  136. # 获取视频最后一秒,生成.jpg
  137. subprocess.run(
  138. ['ffmpeg', '-ss', str(time_offset), '-i', new_video_path, '-t', str(total_duration), '-vf', 'fps=1', "-y", jpg_url])
  139. return jpg_url, height
  140. """
  141. 获取视频音频
  142. """
  143. @classmethod
  144. def get_video_mp3(cls, video_file, video_path_url, pw_random_id):
  145. pw_mp3_path = video_path_url + str(pw_random_id) +'pw_video.mp3'
  146. command = [
  147. 'ffmpeg',
  148. '-i', video_file,
  149. '-q:a', '0',
  150. '-map', 'a',
  151. # '-codec:a', 'libmp3lame', # 指定 MP3 编码器
  152. pw_mp3_path
  153. ]
  154. subprocess.run(command)
  155. time.sleep(1)
  156. return pw_mp3_path
  157. """
  158. 生成片尾视频
  159. """
  160. @classmethod
  161. def pw_video(cls, jpg_url, video_path_url, pw_url, pw_srt, pw_random_id, pw_mp3_path, height):
  162. # 添加音频到图片
  163. """
  164. jpg_url 图片地址
  165. pw_video 提供的片尾视频
  166. pw_duration 提供的片尾视频时长
  167. new_video_path 视频位置
  168. subtitle_cmd 字幕
  169. pw_url 生成视频地址
  170. :return:
  171. """
  172. pw_srt_path = video_path_url + str(pw_random_id) +'pw_video.srt'
  173. # 创建临时字幕文件
  174. with open(pw_srt_path, 'w') as f:
  175. f.write(pw_srt)
  176. # 片尾位置
  177. pw_url_path = video_path_url + str(pw_random_id) + 'pw_video.mp4'
  178. # 获取视频时长
  179. pw_duration = cls.get_video_duration(pw_url)
  180. time.sleep(2)
  181. # 添加字幕 wqy-zenhei Hiragino Sans GB
  182. margin_v = int(height) // 8 # 可根据需要调整字幕和背景之间的距离
  183. subtitle_cmd = f"subtitles={pw_srt_path}:force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=0,PrimaryColour=&H000000,SecondaryColour=&H000000,Bold=1,MarginV={margin_v}'"
  184. bg_position_offset = (int(height) - margin_v) / 1.75
  185. background_cmd = f"drawbox=y=(ih-{int(height)}/2-{bg_position_offset}):color=yellow@1.0:width=iw:height={int(height)}/4:t=fill"
  186. ffmpeg_cmd = [
  187. 'ffmpeg',
  188. '-loop', '1',
  189. '-i', jpg_url, # 输入的图片文件
  190. '-i', pw_mp3_path, # 输入的音频文件
  191. '-c:v', 'libx264', # 视频编码格式
  192. '-t', str(pw_duration), # 输出视频的持续时间,与音频持续时间相同
  193. '-pix_fmt', 'yuv420p', # 像素格式
  194. '-c:a', 'aac', # 音频编码格式
  195. '-strict', 'experimental', # 使用实验性编码器
  196. '-shortest', # 确保输出视频的长度与音频一致
  197. '-vf', f"{background_cmd},{subtitle_cmd}", # 视频过滤器,设置分辨率和其他过滤器
  198. pw_url_path # 输出的视频文件路径
  199. ]
  200. subprocess.run(ffmpeg_cmd)
  201. return pw_url_path
  202. """
  203. 设置统一格式拼接视频
  204. """
  205. @classmethod
  206. def concatenate_videos(cls, video_list, video_path_url):
  207. concatenate_videos_url = video_path_url + 'concatenate_videos.mp4'
  208. # 获取视频的原始宽高信息
  209. width, height = cls.get_w_h_size(video_list[0])
  210. # 拼接视频
  211. VIDEO_COUNTER = 0
  212. FF_INPUT = ""
  213. FF_SCALE = ""
  214. FF_FILTER = ""
  215. ffmpeg_cmd = ["ffmpeg"]
  216. for videos in video_list:
  217. # 添加输入文件
  218. FF_INPUT += f" -i {videos}"
  219. # 为每个视频文件统一长宽,并设置SAR(采样宽高比)
  220. FF_SCALE += f"[{VIDEO_COUNTER}:v]scale={int(height)}x{int(width)},setsar=1[v{VIDEO_COUNTER}];"
  221. # 为每个视频文件创建一个输入流,并添加到-filter_complex参数中
  222. FF_FILTER += f"[v{VIDEO_COUNTER}][{VIDEO_COUNTER}:a]"
  223. # 增加视频计数器
  224. VIDEO_COUNTER += 1
  225. # 构建最终的FFmpeg命令
  226. ffmpeg_cmd.extend(FF_INPUT.split())
  227. ffmpeg_cmd.extend(["-filter_complex", f"{FF_SCALE}{FF_FILTER}concat=n={VIDEO_COUNTER}:v=1:a=1[v][a]",
  228. "-map", "[v]", "-map", "[a]", "-y", concatenate_videos_url])
  229. subprocess.run(ffmpeg_cmd)
  230. return concatenate_videos_url
  231. """
  232. 单个视频拼接
  233. """
  234. @classmethod
  235. def single_video(cls, new_video_path, video_share, video_path_url, zm):
  236. single_video_url = video_path_url + 'single_video.mp4'
  237. single_video_srt = video_path_url + 'single_video.srt'
  238. # 获取时长
  239. duration = cls.get_video_duration(new_video_path)
  240. start_time = cls.seconds_to_srt_time(0)
  241. end_time = cls.seconds_to_srt_time(duration)
  242. single_video_txt = video_path_url + 'single_video.txt'
  243. with open(single_video_txt, 'w') as f:
  244. f.write(f"file '{new_video_path}'\n")
  245. with open(single_video_srt, 'w') as f:
  246. f.write(f"1\n{start_time} --> {end_time}\n\u2764\uFE0F{zm}\n\n")
  247. width, height = cls.get_w_h_size(new_video_path)
  248. box_height = int(int(height) / 4) # 框的高度为视频高度的四分之一
  249. background_cmd = f"drawbox=y=ih-{70 + box_height}-{int(box_height / 20)}:color=yellow@1.0:width=iw:height={box_height}:t=fill"
  250. if video_share == '有':
  251. # 添加字幕 wqy-zenhei Hiragino Sans GB
  252. subtitle_cmd = f"subtitles={single_video_srt}:force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=0,PrimaryColour=&H000000,SecondaryColour=&H000000,Bold=1,MarginV=20'"
  253. draw = f"{background_cmd},{subtitle_cmd}"
  254. else:
  255. subtitle_cmd = f"subtitles={single_video_srt}:force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=20'"
  256. draw = f"{subtitle_cmd}"
  257. # 多线程数
  258. num_threads = 4
  259. # 构建 FFmpeg 命令,生成视频
  260. ffmpeg_cmd_oss = [
  261. "ffmpeg",
  262. "-f", "concat",
  263. "-safe", "0",
  264. "-i", f"{single_video_txt}",
  265. "-c:v", "libx264",
  266. "-c:a", "aac",
  267. "-threads", str(num_threads),
  268. "-vf", f"{draw}",
  269. "-y",
  270. single_video_url
  271. ]
  272. subprocess.run(ffmpeg_cmd_oss)
  273. return single_video_url