ffmpeg.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. import json
  2. import time
  3. import orjson
  4. import requests
  5. from loguru import logger
  6. from protos import task_pb2, task_pb2_grpc
  7. class FFmpeg(object):
  8. def __init__(self, container_id: str, stub: task_pb2_grpc.TaskRunnerStub):
  9. self.container_id = container_id
  10. self.stub = stub
  11. def seconds_to_srt_time(self, seconds):
  12. """
  13. 时间转换
  14. """
  15. hours = int(seconds // 3600)
  16. minutes = int((seconds % 3600) // 60)
  17. seconds = seconds % 60
  18. milliseconds = int((seconds - int(seconds)) * 1000)
  19. return f"{hours:02d}:{minutes:02d}:{int(seconds):02d},{milliseconds:03d}"
  20. # def get_video_duration(self, video_url):
  21. # """
  22. # 获取单个视频时长
  23. # """
  24. # cap = cv2.VideoCapture(video_url)
  25. # if cap.isOpened():
  26. # rate = cap.get(5)
  27. # frame_num = cap.get(7)
  28. # duration = int(frame_num / rate)
  29. # return duration
  30. # return 0
  31. # """
  32. # 获取视频文件的时长(秒)
  33. # """
  34. # def get_videos_duration(self, video_file):
  35. # result = self.asyncio_run_subprocess(["ffprobe", "-v", "error", "-show_entries", "format=duration",
  36. # "-of", "default=noprint_wrappers=1:nokey=1", video_file], timeout=10)
  37. # return float(result)
  38. def get_w_h_size(self, new_video_path):
  39. """
  40. 获取视频宽高
  41. """
  42. try:
  43. # 获取视频的原始宽高信息
  44. request = task_pb2.RunCommandRequest(id=self.container_id, command=["ffprobe", "-v" ,"error" ,"-select_streams" ,"v:0" ,"-show_entries", "stream=width,height" ,"-of" ,"csv=p=0" ,new_video_path])
  45. # ffprobe_cmd = self.asyncio_run_subprocess(["ffprobe", "-v" ,"error" ,"-select_streams" ,"v:0" ,"-show_entries", "stream=width,height" ,"-of" ,"csv=p=0" ,new_video_path],timeout=10)
  46. # output_decoded = ffprobe_cmd.strip()
  47. output_decoded = ''
  48. for response in self.stub.RunCommand(request=request):
  49. output_decoded += response.msg
  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. def video_crop(self, video_path, file_path):
  56. """
  57. 视频裁剪
  58. """
  59. crop_url = file_path + 'crop.mp4'
  60. try:
  61. # # 获取视频的原始宽高信息
  62. # ffprobe_cmd = cls.asyncio_run_subprocess(
  63. # ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of",
  64. # "csv=p=0", video_path], timeout=10)
  65. # width, height = map(int, ffprobe_cmd.strip().split(','))
  66. # # 计算裁剪后的高度
  67. # new_height = int(height * 0.8)
  68. # 构建 FFmpeg 命令,裁剪视频高度为原始高度的80%
  69. request = task_pb2.RunCommandRequest(id=self.container_id, command=["ffmpeg", "-i", video_path, "-vf", f"\"crop=in_w:in_h*0.8\"", "-c:v", "h264_nvenc", "-c:a", "aac", "-y", "-cq", "28", "-preset", "slow", crop_url])
  70. for _ in self.stub.RunCommand(request=request):
  71. pass
  72. # self.asyncio_run_subprocess(
  73. # [
  74. # "ffmpeg",
  75. # "-i", video_path,
  76. # "-vf", f"crop={width}:{new_height}",
  77. # "-c:v", "libx264",
  78. # "-c:a", "aac",
  79. # "-y",
  80. # crop_url
  81. # ],timeout=240)
  82. return crop_url
  83. except Exception as e:
  84. return crop_url
  85. def video_ggduration(self, video_path, file_path, gg_duration_total):
  86. """
  87. 视频截断
  88. """
  89. gg_duration_url = file_path + 'gg_duration.mp4'
  90. # 获取视频时长
  91. try:
  92. total_duration = self.get_duration(video_path)
  93. if total_duration == 0:
  94. return gg_duration_url
  95. duration = int(total_duration) - int(gg_duration_total)
  96. if int(total_duration) < int(gg_duration_total):
  97. return gg_duration_url
  98. request = task_pb2.RunCommandRequest(id=self.container_id, command=["ffmpeg", "-i", video_path, "-c:v", "h264_nvenc", "-c:a", "aac", "-t", str(duration), "-y", "-cq", "28", "-preset", "slow", gg_duration_url])
  99. for _ in self.stub.RunCommand(request=request):
  100. pass
  101. # self.asyncio_run_subprocess([
  102. # "ffmpeg",
  103. # "-i", video_path,
  104. # "-c:v", "libx264",
  105. # "-c:a", "aac",
  106. # "-t", str(duration),
  107. # "-y",
  108. # gg_duration_url
  109. # ], timeout= 360)
  110. return gg_duration_url
  111. except Exception as e:
  112. return gg_duration_url
  113. def video_png(self, video_path, file_path):
  114. """
  115. 截取原视频最后一帧
  116. """
  117. # 获取视频的原始宽高信息
  118. jpg_url = file_path + 'png.jpg'
  119. try:
  120. # self.asyncio_run_subprocess(
  121. # ["ffmpeg", "-sseof", "-1", '-i', video_path, '-frames:v', '1', "-y", jpg_url], timeout=120)
  122. request = task_pb2.RunCommandRequest(id=self.container_id, command=["ffmpeg", "-sseof", "-1", '-i', video_path, '-frames:v', '1', "-y", jpg_url])
  123. for _ in self.stub.RunCommand(request=request):
  124. pass
  125. return jpg_url
  126. except Exception as e:
  127. return jpg_url
  128. def get_video_mp3(self, video_file, video_path_url, pw_random_id):
  129. """
  130. 获取视频音频
  131. """
  132. pw_mp3_path = video_path_url + str(pw_random_id) +'pw_video.mp3'
  133. try:
  134. request = task_pb2.RunCommandRequest(id=self.container_id, command=['ffmpeg', '-i', video_file, '-q:a', '0', '-map', 'a', pw_mp3_path])
  135. for _ in self.stub.RunCommand(request=request):
  136. pass
  137. # self.asyncio_run_subprocess([
  138. # 'ffmpeg',
  139. # '-i', video_file,
  140. # '-q:a', '0',
  141. # '-map', 'a',
  142. # pw_mp3_path
  143. # ], timeout=120)
  144. time.sleep(1)
  145. return pw_mp3_path
  146. except Exception as e:
  147. return pw_mp3_path
  148. def get_pw_video_mp3(self, video_file, video_path_url):
  149. bgm_pw_path_mp3 = video_file + 'bgm_pw.mp3'
  150. try:
  151. request = task_pb2.RunCommandRequest(id=self.container_id, command=['ffmpeg', '-i', video_path_url, '-q:a', '0', '-map', 'a', bgm_pw_path_mp3])
  152. for _ in self.stub.RunCommand(request=request):
  153. pass
  154. # self.asyncio_run_subprocess([
  155. # 'ffmpeg',
  156. # '-i', video_path_url, # 输入的视频文件路径
  157. # '-q:a', '0', # 设置音频质量为最佳
  158. # '-map', 'a',
  159. # '-y',
  160. # bgm_pw_path_mp3
  161. # ], timeout=120)
  162. # time.sleep(1)
  163. return bgm_pw_path_mp3
  164. except Exception as e:
  165. return bgm_pw_path_mp3
  166. def video_add_bgm(self, video_file, bgm_path, video_path_url):
  167. """
  168. 片尾增加bgm
  169. """
  170. bgm_pw_path = video_path_url + 'bgm_pw.mp4'
  171. pw_duration = self.get_duration(video_file)
  172. try:
  173. pw_path_txt = video_path_url + 'bgm_pw_video.txt'
  174. # with open(pw_path_txt, 'w') as f:
  175. # f.write(f"file '{video_file}'\n")
  176. payload = f"file '{video_file}'\n".encode()
  177. request = task_pb2.PutFileRequest(id=self.container_id, payload=payload, path=pw_path_txt)
  178. self.stub.PutFile(request=request)
  179. request = task_pb2.RunCommandRequest(id=self.container_id,
  180. command=[
  181. "ffmpeg",
  182. "-f", "concat",
  183. "-safe", "0",
  184. "-i", f"{pw_path_txt}", # 视频序列输入的文本文件
  185. "-i", bgm_path, # 音频文件
  186. "-c:v", "h264_nvenc", # 视频编码格式
  187. '-c:a', 'aac', # 音频编码格式
  188. "-t", str(pw_duration), # 输出视频的持续时间
  189. '-b:v', '260k', # 视频比特率
  190. '-b:a', '96k', # 音频比特率
  191. '-threads', '2', # 线程数
  192. # '-vf', f'{background_cmd},{subtitle_cmd}', # 视频过滤器(背景和字幕)
  193. "-filter_complex", "\"[1:a]volume=0.6[a1];[0:a][a1]amerge=inputs=2[aout]\"", # 混合音频流
  194. "-map", "\"0:v:0\"", # 映射视频流来自第一个输入文件(视频)
  195. "-map", "\"[aout]\"", # 映射混合后的音频流
  196. '-y', # 强制覆盖输出文件
  197. "-cq", "28",
  198. "-preset", "slow",
  199. bgm_pw_path # 输出文件路径
  200. ])
  201. for _ in self.stub.RunCommand(request=request):
  202. pass
  203. # self.asyncio_run_subprocess([
  204. # "ffmpeg",
  205. # "-f", "concat",
  206. # "-safe", "0",
  207. # "-i", f"{pw_path_txt}", # 视频序列输入的文本文件
  208. # "-i", bgm_path, # 音频文件
  209. # "-c:v", "libx264", # 视频编码格式
  210. # "-t", str(pw_duration), # 输出视频的持续时间
  211. # '-c:a', 'aac', # 音频编码格式
  212. # '-b:v', '260k', # 视频比特率
  213. # '-b:a', '96k', # 音频比特率
  214. # '-threads', '2', # 线程数
  215. # # '-vf', f'{background_cmd},{subtitle_cmd}', # 视频过滤器(背景和字幕)
  216. # "-filter_complex", "[1:a]volume=0.6[a1];[0:a][a1]amerge=inputs=2[aout]", # 混合音频流
  217. # "-map", "0:v:0", # 映射视频流来自第一个输入文件(视频)
  218. # "-map", "[aout]", # 映射混合后的音频流
  219. # '-y', # 强制覆盖输出文件
  220. # bgm_pw_path # 输出文件路径
  221. # ], timeout=500)
  222. # time.sleep(1)
  223. return bgm_pw_path
  224. except Exception as e:
  225. return bgm_pw_path
  226. def update_video_h_w(self, video_path, file_path):
  227. """横屏视频改为竖屏"""
  228. video_h_w_path = file_path +'video_h_w_video.mp4'
  229. try:
  230. request = task_pb2.RunCommandRequest(id=self.container_id, command=["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])
  231. for _ in self.stub.RunCommand(request=request):
  232. pass
  233. # self.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)
  234. return video_h_w_path
  235. except Exception as e:
  236. return video_h_w_path
  237. def video_640(self, video_path, file_path):
  238. """视频转为640像素"""
  239. video_url = file_path + 'pixelvideo.mp4'
  240. try:
  241. request = task_pb2.RunCommandRequest(id=self.container_id, command=["ffmpeg" ,"-i" ,video_path ,"-vf" ,"\"scale=360:640\"" ,video_url])
  242. for _ in self.stub.RunCommand(request=request):
  243. pass
  244. # self.asyncio_run_subprocess(["ffmpeg" ,"-i" ,video_path ,"-vf" ,"scale=360:640" ,video_url],timeout=420)
  245. return video_url
  246. except Exception as e:
  247. return video_url
  248. def concatenate_videos(self, videos_paths, file_path):
  249. video_url = file_path + 'rg_pw.mp4'
  250. list_filename = file_path + 'rg_pw.txt'
  251. # with open(list_filename, "w") as f:
  252. # for video_path in videos_paths:
  253. # f.write(f"file '{video_path}'\n")
  254. payload = b''
  255. for video_path in videos_paths:
  256. payload += f"file '{video_path}'\n".encode()
  257. request = task_pb2.PutFileRequest(id=self.container_id, payload=payload, path=list_filename)
  258. self.stub.PutFile(request=request)
  259. try:
  260. request = task_pb2.RunCommandRequest(id=self.container_id, command=["ffmpeg", "-f", "concat", "-safe", "0", "-i", list_filename, "-c", "copy", video_url])
  261. for _ in self.stub.RunCommand(request=request):
  262. pass
  263. # self.asyncio_run_subprocess(
  264. # ["ffmpeg", "-f", "concat", "-safe", "0", "-i", list_filename, "-c", "copy", video_url], timeout=420)
  265. logger.info(f"[+] 视频转为640像素成功")
  266. return video_url
  267. except Exception as e:
  268. return video_url
  269. def h_b_video(self, video_path, pw_path, file_path):
  270. """视频拼接到一起"""
  271. video_url = file_path + 'hbvideo.mp4'
  272. try:
  273. request = task_pb2.RunCommandRequest(id=self.container_id, command=["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])
  274. for _ in self.stub.RunCommand(request=request):
  275. pass
  276. # self.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)
  277. return video_url
  278. except Exception as e:
  279. return video_url
  280. def add_video_zm(self, new_video_path, video_path_url, pw_random_id, new_text):
  281. """横屏视频顶部增加字幕"""
  282. single_video_srt = video_path_url + str(pw_random_id) +'video_zm.srt'
  283. single_video_txt = video_path_url + str(pw_random_id) +'video_zm.txt'
  284. single_video = video_path_url + str(pw_random_id) +'video_zm.mp4'
  285. try:
  286. duration = self.get_duration(new_video_path)
  287. if duration == 0:
  288. return new_video_path
  289. start_time = self.seconds_to_srt_time(0)
  290. end_time = self.seconds_to_srt_time(duration)
  291. # zm = '致敬伟大的教员,为整个民族\n感谢老人家历史向一代伟人'
  292. payload = f"file '{new_video_path}'\n".encode()
  293. request = task_pb2.PutFileRequest(id=self.container_id, payload=payload, path=single_video_txt)
  294. self.stub.PutFile(request)
  295. # with open(single_video_txt, 'w') as f:
  296. # f.write(f"file '{new_video_path}'\n")
  297. payload = f"1\n{start_time} --> {end_time}\n{new_text}\n\n".encode()
  298. request = task_pb2.PutFileRequest(id=self.container_id, payload=payload, path=single_video_srt)
  299. self.stub.PutFile(request)
  300. # with open(single_video_srt, 'w') as f:
  301. # f.write(f"1\n{start_time} --> {end_time}\n{new_text}\n\n")
  302. subtitle_cmd = f"subtitles={single_video_srt}:force_style='Fontsize=12,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=225'"
  303. request = task_pb2.RunCommandRequest(id=self.container_id,
  304. command=[
  305. "ffmpeg",
  306. "-f", "concat",
  307. "-safe", "0",
  308. "-i", single_video_txt,
  309. "-c:v", "h264_nvenc",
  310. "-c:a", "aac",
  311. "-vf", f"\"{subtitle_cmd}\"",
  312. "-y",
  313. "-cq", "28",
  314. "-preset", "slow",
  315. single_video
  316. ])
  317. for _ in self.stub.RunCommand(request=request):
  318. pass
  319. # self.asyncio_run_subprocess([
  320. # "ffmpeg",
  321. # "-f", "concat",
  322. # "-safe", "0",
  323. # "-i", single_video_txt,
  324. # "-c:v", "libx264",
  325. # "-c:a", "aac",
  326. # "-vf", draw,
  327. # "-y",
  328. # single_video
  329. # ],timeout=500)
  330. # subprocess.run(ffmpeg_cmd)
  331. return single_video
  332. except Exception as e:
  333. return single_video
  334. def get_duration(self, file_path):
  335. """获取mp3时长"""
  336. # audio = MP3(file_path)
  337. # duration = audio.info.length
  338. # if duration:
  339. # return int(duration)
  340. request = task_pb2.RunCommandRequest(id=self.container_id, command=['ffprobe', '-show_format', '-show_streams', '-of', 'json', '-v', 'quiet', '-hide_banner', file_path])
  341. msg = ''
  342. for response in self.stub.RunCommand(request):
  343. msg += response.msg
  344. obj = orjson.loads(msg)
  345. return int(float(obj['format']['duration']))
  346. def pw_video(self, jpg_path, file_path, pw_mp3_path, pw_srt):
  347. """
  348. 生成片尾视频
  349. jpg_url 图片地址
  350. pw_video 提供的片尾视频
  351. pw_duration 提供的片尾视频时长
  352. new_video_path 视频位置
  353. subtitle_cmd 字幕
  354. pw_url 生成视频地址
  355. :return:
  356. """
  357. # 添加音频到图片
  358. pw_srt_path = file_path +'pw_video.srt'
  359. payload = pw_srt.encode()
  360. request = task_pb2.PutFileRequest(id=self.container_id, payload=payload, path=pw_srt_path)
  361. self.stub.PutFile(request=request)
  362. # with open(pw_srt_path, 'w') as f:
  363. # f.write(pw_srt)
  364. pw_url_path = file_path + 'pw_video.mp4'
  365. try:
  366. pw_duration = self.get_duration(pw_mp3_path)
  367. if pw_duration == 0:
  368. return pw_url_path
  369. time.sleep(2)
  370. # 添加字幕 wqy-zenhei Hiragino Sans GB
  371. height = 1080
  372. margin_v = int(height) // 8 # 可根据需要调整字幕和背景之间的距离
  373. 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}'"
  374. bg_position_offset = (int(360) - 360//8) / 1.75
  375. background_cmd = f"drawbox=y=(ih-{int(360)}/2-{bg_position_offset}):color=yellow@1.0:width=iw:height={int(360)}/4:t=fill"
  376. if "mp4" in jpg_path:
  377. pw_path_txt = file_path + 'pw_path_video.txt'
  378. payload = f"file '{jpg_path}'\n".encode()
  379. request = task_pb2.PutFileRequest(id=self.container_id, payload=payload, path=pw_path_txt)
  380. self.stub.PutFile(request=request)
  381. # with open(pw_path_txt, 'w') as f:
  382. # f.write(f"file '{jpg_path}'\n")
  383. request = task_pb2.RunCommandRequest(id=self.container_id,
  384. command=[
  385. "ffmpeg",
  386. "-f", "concat",
  387. "-safe", "0",
  388. "-i", f"{pw_path_txt}", # 视频序列输入的文本文件
  389. "-i", pw_mp3_path, # 音频文件
  390. "-c:v", "h264_nvenc", # 视频编码格式
  391. "-t", str(pw_duration), # 输出视频的持续时间
  392. "-c:a", "aac", # 音频编码格式
  393. "-b:v", "260k", # 视频比特率
  394. "-b:a", "96k", # 音频比特率
  395. "-threads", "2", # 线程数
  396. "-vf", f"\"{background_cmd},{subtitle_cmd}\"", # 视频过滤器(背景和字幕)
  397. "-map", "0:v:0", # 映射视频流来自第一个输入文件(视频)
  398. "-map", "1:a:0", # 映射音频流来自第二个输入文件(音频)
  399. "-y", # 强制覆盖输出文件
  400. "-cq", "28",
  401. "-preset", "slow",
  402. pw_url_path # 输出文件路径
  403. ])
  404. for _ in self.stub.RunCommand(request=request):
  405. pass
  406. # self.asyncio_run_subprocess([
  407. # "ffmpeg",
  408. # "-f", "concat",
  409. # "-safe", "0",
  410. # "-i", f"{pw_path_txt}", # 视频序列输入的文本文件
  411. # "-i", pw_mp3_path, # 音频文件
  412. # "-c:v", "libx264", # 视频编码格式
  413. # "-t", str(pw_duration), # 输出视频的持续时间
  414. # "-c:a", "aac", # 音频编码格式
  415. # "-b:v", "260k", # 视频比特率
  416. # "-b:a", "96k", # 音频比特率
  417. # "-threads", "2", # 线程数
  418. # "-vf", f"{background_cmd},{subtitle_cmd}", # 视频过滤器(背景和字幕)
  419. # "-map", "0:v:0", # 映射视频流来自第一个输入文件(视频)
  420. # "-map", "1:a:0", # 映射音频流来自第二个输入文件(音频)
  421. # "-y", # 强制覆盖输出文件
  422. # pw_url_path # 输出文件路径
  423. # ], timeout=500)
  424. else:
  425. request = task_pb2.RunCommandRequest(id=self.container_id,
  426. command=[
  427. 'ffmpeg',
  428. '-loop', '1',
  429. '-i', jpg_path, # 输入的图片文件
  430. '-i', pw_mp3_path, # 输入的音频文件
  431. '-c:v', 'h264_nvenc', # 视频编码格式
  432. '-t', str(pw_duration), # 输出视频的持续时间,与音频持续时间相同
  433. '-pix_fmt', 'yuv420p', # 像素格式
  434. '-c:a', 'aac', # 音频编码格式
  435. '-strict', 'experimental', # 使用实验性编码器
  436. '-shortest', # 确保输出视频的长度与音频一致
  437. '-vf', f"\"{background_cmd},{subtitle_cmd}\"", # 视频过滤器,设置分辨率和其他过滤器
  438. "-cq", "28",
  439. "-preset", "slow",
  440. pw_url_path # 输出的视频文件路径
  441. ])
  442. for _ in self.stub.RunCommand(request=request):
  443. pass
  444. # self.asyncio_run_subprocess([
  445. # 'ffmpeg',
  446. # '-loop', '1',
  447. # '-i', jpg_path, # 输入的图片文件
  448. # '-i', pw_mp3_path, # 输入的音频文件
  449. # '-c:v', 'libx264', # 视频编码格式
  450. # '-t', str(pw_duration), # 输出视频的持续时间,与音频持续时间相同
  451. # '-pix_fmt', 'yuv420p', # 像素格式
  452. # '-c:a', 'aac', # 音频编码格式
  453. # '-strict', 'experimental', # 使用实验性编码器
  454. # '-shortest', # 确保输出视频的长度与音频一致
  455. # '-vf', f"{background_cmd},{subtitle_cmd}", # 视频过滤器,设置分辨率和其他过滤器
  456. # pw_url_path # 输出的视频文件路径
  457. # ], timeout=500)
  458. # if os.path.exists(pw_srt_path):
  459. # os.remove(pw_srt_path)
  460. return pw_url_path
  461. except Exception as e:
  462. return pw_url_path
  463. def single_video(self, video_path, file_path, zm):
  464. """
  465. 单个视频拼接
  466. """
  467. single_video_url = file_path + 'single_video.mp4'
  468. single_video_srt = file_path + 'single_video.srt'
  469. # 获取时长
  470. try:
  471. duration = self.get_duration(video_path)
  472. if duration == 0:
  473. return single_video_url
  474. start_time = self.seconds_to_srt_time(2)
  475. end_time = self.seconds_to_srt_time(duration)
  476. single_video_txt = file_path + 'single_video.txt'
  477. # with open(single_video_txt, 'w') as f:
  478. # f.write(f"file '{video_path}'\n")
  479. payload = f"file '{video_path}'\n".encode()
  480. request = task_pb2.PutFileRequest(id=self.container_id, payload=payload, path=single_video_txt)
  481. self.stub.PutFile(request=request)
  482. if zm:
  483. payload = f"1\n{start_time} --> {end_time}\n<font color=\"red\">\u2764\uFE0F</font>{zm}\n\n".encode()
  484. request = task_pb2.PutFileRequest(id=self.container_id, payload=payload, path=single_video_srt)
  485. self.stub.PutFile(request=request)
  486. # with open(single_video_srt, 'w') as f:
  487. # f.write(f"1\n{start_time} --> {end_time}\n<font color=\"red\">\u2764\uFE0F</font>{zm}\n\n")
  488. subtitle_cmd = f"subtitles={single_video_srt}:force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=20'"
  489. else:
  490. subtitle_cmd = f"force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=20'"
  491. # 多线程数
  492. num_threads = 5
  493. # 构建 FFmpeg 命令,生成视频
  494. request = task_pb2.RunCommandRequest(id=self.container_id,
  495. command=[
  496. "ffmpeg",
  497. "-f", "concat",
  498. "-safe", "0",
  499. "-i", f"{single_video_txt}",
  500. "-c:v", "h264_nvenc",
  501. "-c:a", "aac",
  502. '-b:v', '260k',
  503. "-b:a", "96k",
  504. "-threads", str(num_threads),
  505. "-vf", f"\"{subtitle_cmd}\"",
  506. "-y",
  507. "-cq", "28",
  508. "-preset", "slow",
  509. single_video_url
  510. ])
  511. for _ in self.stub.RunCommand(request=request):
  512. pass
  513. # self.asyncio_run_subprocess([
  514. # "ffmpeg",
  515. # "-f", "concat",
  516. # "-safe", "0",
  517. # "-i", f"{single_video_txt}",
  518. # "-c:v", "libx264",
  519. # "-c:a", "aac",
  520. # '-b:v', '260k',
  521. # "-b:a", "96k",
  522. # "-threads", str(num_threads),
  523. # "-vf", subtitle_cmd,
  524. # "-y",
  525. # single_video_url
  526. # ], timeout=400)
  527. # if os.path.exists(single_video_srt):
  528. # os.remove(single_video_srt)
  529. return single_video_url
  530. except Exception as e:
  531. return single_video_url
  532. # def asyncio_run_subprocess(self, params: List[str], timeout: int = 30) -> str:
  533. # async def run_subprocess():
  534. # process = await asyncio.create_subprocess_exec(
  535. # params[0],
  536. # *params[1:],
  537. # stdout=asyncio.subprocess.PIPE,
  538. # stderr=asyncio.subprocess.PIPE,
  539. # )
  540. # try:
  541. # out, err = await asyncio.wait_for(process.communicate(), timeout=timeout)
  542. # if process.returncode != 0:
  543. # raise IOError(err)
  544. # return out.decode()
  545. # except asyncio.TimeoutError:
  546. # process.kill()
  547. # out, err = await process.communicate()
  548. # raise IOError(err)
  549. # return asyncio.run(run_subprocess())
  550. def get_http_duration(self, videos_path):
  551. total_duration = 0
  552. for video_path in videos_path:
  553. url = "http://101.37.24.17:5555/api/v1/ffmpeg/get_meta"
  554. payload = json.dumps({
  555. "url": video_path,
  556. "referer": ""
  557. })
  558. headers = {
  559. 'Authorization': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNGNhMTI4ZGYtYWMzMy00NWQ2LTg3MmEtMDAzOTk4MGVhM2ViIiwibmFtZSI6Inp5IiwiZXhwIjoyMDUwOTI3MjExfQ.k_rvuESjA62RgPDiLniVgJyLJn3Q8C1Y_AGq3CPRuKI',
  560. 'Content-Type': 'application/json'
  561. }
  562. try:
  563. response = requests.request("POST", url, headers=headers, data=payload, timeout=30)
  564. response = response.json()
  565. duration = response['data']['streams'][0]['duration']
  566. total_duration += int(float(duration))
  567. except Exception as e:
  568. print(f"Error processing {video_path}: {e}")
  569. return total_duration
  570. if __name__ == '__main__':
  571. file_path = '/Users/z/Downloads/478de0b6-4e52-44a5-a5d4-967b2cf8ce49'
  572. jpg_path = '/Users/z/Downloads/478de0b6-4e52-44a5-a5d4-967b2cf8ce49rg_pixelvideo.mp4'
  573. mp3_path='/Users/z/Downloads/478de0b6-4e52-44a5-a5d4-967b2cf8ce49pw_video.mp3'
  574. pw_srt = """1
  575. 00:00:00,000 --> 00:00:02,842
  576. 这个视频揭示了中国近代历史上
  577. 2
  578. 00:00:02,842 --> 00:00:05,685
  579. 一个鲜为人知却又极为重要的故
  580. 3
  581. 00:00:05,685 --> 00:00:05,888
  582. 4
  583. 00:00:05,888 --> 00:00:07,106
  584. 真是让人震惊
  585. 5
  586. 00:00:07,106 --> 00:00:07,715
  587. 看完后
  588. 6
  589. 00:00:07,715 --> 00:00:10,354
  590. 我不禁对历史有了更深的思考
  591. 7
  592. 00:00:10,354 --> 00:00:12,588
  593. 让我们一起重温这段历史
  594. 8
  595. 00:00:12,588 --> 00:00:14,212
  596. 提醒自己珍惜当下
  597. 9
  598. 00:00:14,212 --> 00:00:17,055
  599. 我相信很多朋友也会对这个话题
  600. 10
  601. 00:00:17,055 --> 00:00:17,664
  602. 感兴趣
  603. 11
  604. 00:00:17,664 --> 00:00:20,506
  605. 请把这个视频分享到你们的群聊
  606. 12
  607. 00:00:20,506 --> 00:00:20,709
  608. 13
  609. 00:00:20,709 --> 00:00:22,740
  610. 让更多人了解这段历史
  611. 14
  612. 00:00:22,820 --> 00:00:23,824
  613. 共鸣与反思
  614. 15
  615. 00:00:23,824 --> 00:00:25,430
  616. 是我们共同的责任
  617. 16
  618. 00:00:25,430 --> 00:00:28,242
  619. 也许我们能从中汲取更多的智慧
  620. 17
  621. 00:00:28,242 --> 00:00:28,844
  622. 与力量
  623. 18
  624. 00:00:28,844 --> 00:00:29,848
  625. 快动动手指
  626. 19
  627. 00:00:29,848 --> 00:00:32,659
  628. 让我们一起分享这段重要的历史
  629. 20
  630. 00:00:32,659 --> 00:00:32,860
  631. 吧"""
  632. FFmpeg.pw_video(jpg_path, file_path, mp3_path, pw_srt)