|
@@ -15,6 +15,17 @@ from urllib.parse import urlencode
|
|
|
|
|
|
class FFmpeg():
|
|
|
|
|
|
+ """
|
|
|
+ 时间转换
|
|
|
+ """
|
|
|
+ @classmethod
|
|
|
+ def seconds_to_srt_time(cls, seconds):
|
|
|
+ hours = int(seconds // 3600)
|
|
|
+ minutes = int((seconds % 3600) // 60)
|
|
|
+ seconds = seconds % 60
|
|
|
+ milliseconds = int((seconds - int(seconds)) * 1000)
|
|
|
+ return f"{hours:02d}:{minutes:02d}:{int(seconds):02d},{milliseconds:03d}"
|
|
|
+
|
|
|
"""
|
|
|
获取单个视频时长
|
|
|
"""
|
|
@@ -47,82 +58,99 @@ class FFmpeg():
|
|
|
@classmethod
|
|
|
def video_tailor(cls, video_url):
|
|
|
output_video_path = ''
|
|
|
- # 获取视频的原始宽高信息
|
|
|
- ffprobe_cmd = f"ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 {video_url}"
|
|
|
- ffprobe_process = subprocess.Popen(ffprobe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
|
|
- output, _ = ffprobe_process.communicate()
|
|
|
- width, height = map(int, output.decode().strip().split(','))
|
|
|
- # 计算裁剪后的高度
|
|
|
- new_height = int(height * 0.7)
|
|
|
-
|
|
|
- # 构建 FFmpeg 命令,裁剪视频高度为原始高度的70%,并将宽度缩放为320x480
|
|
|
- ffmpeg_cmd = [
|
|
|
- "ffmpeg",
|
|
|
- "-i", video_url,
|
|
|
- "-vf", f"crop={width}:{new_height},scale=320:480",
|
|
|
- "-c:v", "libx264",
|
|
|
- "-c:a", "aac",
|
|
|
- "-y",
|
|
|
- output_video_path
|
|
|
- ]
|
|
|
-
|
|
|
- # 执行 FFmpeg 命令
|
|
|
- subprocess.run(ffmpeg_cmd)
|
|
|
- return output_video_path
|
|
|
+ try:
|
|
|
+ # 获取视频的原始宽高信息
|
|
|
+ ffprobe_cmd = f"ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 {video_url}"
|
|
|
+ ffprobe_process = subprocess.Popen(ffprobe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
|
|
+ output, _ = ffprobe_process.communicate()
|
|
|
+ width, height = map(int, output.decode().strip().split(','))
|
|
|
+ # 计算裁剪后的高度
|
|
|
+ new_height = int(height * 0.7)
|
|
|
+ # 构建 FFmpeg 命令,裁剪视频高度为原始高度的70%,并将宽度缩放为320x480
|
|
|
+ ffmpeg_cmd = [
|
|
|
+ "ffmpeg",
|
|
|
+ "-i", video_url,
|
|
|
+ "-vf", f"crop={width}:{new_height},scale=320:480",
|
|
|
+ "-c:v", "libx264",
|
|
|
+ "-c:a", "aac",
|
|
|
+ "-y",
|
|
|
+ output_video_path
|
|
|
+ ]
|
|
|
+ # 执行 FFmpeg 命令
|
|
|
+ subprocess.run(ffmpeg_cmd, check=True)
|
|
|
+ return output_video_path
|
|
|
+ except Exception as e:
|
|
|
+ return None
|
|
|
|
|
|
"""
|
|
|
截取原视频最后一帧
|
|
|
"""
|
|
|
@classmethod
|
|
|
- def video_png(cls,video_url):
|
|
|
+ def video_png(cls, new_video_path, video_path_url):
|
|
|
"""
|
|
|
- png 生成图片位置
|
|
|
- :param video_url: 视频地址
|
|
|
+ jpg_url 生成图片位置
|
|
|
+ :param new_video_path: 视频地址
|
|
|
:return:
|
|
|
"""
|
|
|
- png = ''
|
|
|
- # 获取视频时长
|
|
|
- total_duration = cls.get_video_duration(video_url)
|
|
|
- time_offset = total_duration - 1 # 提取倒数第一秒的帧
|
|
|
- # 获取视频最后一秒,生成.jpg
|
|
|
- subprocess.run(
|
|
|
- ['ffmpeg', '-ss', str(time_offset), '-i', video_url, '-t', str(total_duration), '-vf', 'fps=1', png])
|
|
|
- return png
|
|
|
+ try:
|
|
|
+ jpg_url = video_path_url + 'png.jpg'
|
|
|
+ # 获取视频时长
|
|
|
+ total_duration = cls.get_video_duration(new_video_path)
|
|
|
+ time_offset = total_duration - 1 # 提取倒数第一秒的帧
|
|
|
+ # 获取视频最后一秒,生成.jpg
|
|
|
+ subprocess.run(
|
|
|
+ ['ffmpeg', '-ss', str(time_offset), '-i', new_video_path, '-t', str(total_duration), '-vf', 'fps=1', "-y", jpg_url])
|
|
|
+ return jpg_url
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ return None
|
|
|
|
|
|
"""
|
|
|
生成片尾视频
|
|
|
"""
|
|
|
@classmethod
|
|
|
- def new_pw_video(cls):
|
|
|
+ def pw_video(cls, jpg_url, video_path_url, pw_url, pw_srt):
|
|
|
# 添加音频到图片
|
|
|
"""
|
|
|
- png_path 图片地址
|
|
|
+ jpg_url 图片地址
|
|
|
pw_video 提供的片尾视频
|
|
|
pw_duration 提供的片尾视频时长
|
|
|
- background_cmd 视频位置
|
|
|
+ new_video_path 视频位置
|
|
|
subtitle_cmd 字幕
|
|
|
- pw_path 生成视频地址
|
|
|
+ pw_url 生成视频地址
|
|
|
:return:
|
|
|
"""
|
|
|
- ffmpeg_cmd = ['ffmpeg', '-loop', '1', '-i', png_path, '-i', pw_video, '-c:v', 'libx264', '-t',
|
|
|
+ pw_srt_path = video_path_url + 'pw_video.srt'
|
|
|
+ # 创建临时字幕文件
|
|
|
+ with open(pw_srt_path, 'w') as f:
|
|
|
+ f.write(pw_srt)
|
|
|
+ # 片尾位置
|
|
|
+ pw_url_path = video_path_url + 'pw_video.mp4'
|
|
|
+ # 获取视频时长
|
|
|
+ pw_duration = cls.get_video_duration(pw_url)
|
|
|
+ # 添加字幕 wqy-zenhei Hiragino Sans GB
|
|
|
+ subtitle_cmd = f"subtitles={pw_srt_path}:force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=0,PrimaryColour=&H000000,SecondaryColour=&H000000,Bold=1,MarginV=155'"
|
|
|
+ background_cmd = "drawbox=y=366/2:color=yellow@1.0:width=iw:height=50:t=fill"
|
|
|
+ ffmpeg_cmd = ['ffmpeg', '-loop', '1', '-i', jpg_url, '-i', pw_url, '-c:v', 'libx264', '-t',
|
|
|
str(pw_duration), '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-strict', 'experimental', '-shortest',
|
|
|
- '-vf', f"scale=320x480,{background_cmd},{subtitle_cmd}", pw_path]
|
|
|
+ '-vf', f"scale=320x480,{background_cmd},{subtitle_cmd}", pw_url_path]
|
|
|
subprocess.run(ffmpeg_cmd)
|
|
|
- return pw_path
|
|
|
+ return pw_url_path
|
|
|
|
|
|
|
|
|
"""
|
|
|
设置统一格式拼接视频
|
|
|
"""
|
|
|
@classmethod
|
|
|
- def concatenate_videos(cls, video_files, concatenated_video_path):
|
|
|
+ def concatenate_videos(cls, video_list, video_path_url):
|
|
|
+ concatenate_videos_url = video_path_url + 'concatenate_videos.mp4'
|
|
|
# 拼接视频
|
|
|
VIDEO_COUNTER = 0
|
|
|
FF_INPUT = ""
|
|
|
FF_SCALE = ""
|
|
|
FF_FILTER = ""
|
|
|
ffmpeg_cmd = ["ffmpeg"]
|
|
|
- for videos in video_files:
|
|
|
+ for videos in video_list:
|
|
|
# 添加输入文件
|
|
|
FF_INPUT += f" -i {videos}"
|
|
|
# 为每个视频文件统一长宽,并设置SAR(采样宽高比)
|
|
@@ -134,95 +162,54 @@ class FFmpeg():
|
|
|
# 构建最终的FFmpeg命令
|
|
|
ffmpeg_cmd.extend(FF_INPUT.split())
|
|
|
ffmpeg_cmd.extend(["-filter_complex", f"{FF_SCALE}{FF_FILTER}concat=n={VIDEO_COUNTER}:v=1:a=1[v][a]",
|
|
|
- "-map", "[v]", "-map", "[a]", concatenated_video_path])
|
|
|
+ "-map", "[v]", "-map", "[a]", "-y", concatenate_videos_url])
|
|
|
subprocess.run(ffmpeg_cmd)
|
|
|
-
|
|
|
- return concatenated_video_path
|
|
|
+ return concatenate_videos_url
|
|
|
|
|
|
|
|
|
"""
|
|
|
单个视频拼接
|
|
|
"""
|
|
|
@classmethod
|
|
|
- def single_video(cls):
|
|
|
- text_ptah = cls.bk_text_folders(mark)
|
|
|
- video_files = cls.concat_videos_with_subtitles(videos, audio_duration, platform, mark)
|
|
|
- with open(text_ptah, 'w') as f:
|
|
|
- for file in video_files:
|
|
|
- f.write(f"file '{file}'\n")
|
|
|
- if video_files == "":
|
|
|
- return ""
|
|
|
- if os.path.exists(s_path):
|
|
|
- # subtitle_cmd = f"subtitles={s_path}:force_style='Fontsize=11,Fontname=Hiragino Sans GB,Outline=0,PrimaryColour=&H000000,SecondaryColour=&H000000'"
|
|
|
- subtitle_cmd = f"subtitles={s_path}:force_style='Fontsize=12,Fontname=wqy-zenhei,Bold=1,Outline=0,PrimaryColour=&H000000,SecondaryColour=&H000000'"
|
|
|
+ def single_video(cls, new_video_path, video_share, video_path_url, zm):
|
|
|
+ single_video_url = video_path_url + 'single_video.mp4'
|
|
|
+ single_video_srt = video_path_url + 'single_video.srt'
|
|
|
+ # 获取时长
|
|
|
+ duration = cls.get_video_duration(new_video_path)
|
|
|
+ start_time = cls.seconds_to_srt_time(0)
|
|
|
+ end_time = cls.seconds_to_srt_time(duration)
|
|
|
+ single_video_txt = video_path_url + 'single_video.txt'
|
|
|
+ with open(single_video_txt, 'w') as f:
|
|
|
+ f.write(f"file '{new_video_path}'\n")
|
|
|
+ with open(single_video_srt, 'w') as f:
|
|
|
+ f.write(f"1\n{start_time} --> {end_time}\n\u2764\uFE0F{zm}\n\n")
|
|
|
+ background_cmd = "drawbox=y=370/1:color=yellow@1.0:width=iw:height=70:t=fill"
|
|
|
+ if video_share == '有':
|
|
|
+ # 添加字幕 wqy-zenhei Hiragino Sans GB
|
|
|
+ subtitle_cmd = f"subtitles={single_video_srt}:force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=0,PrimaryColour=&H000000,SecondaryColour=&H000000,Bold=1,MarginV=30'"
|
|
|
+ draw = f"{background_cmd},{subtitle_cmd}"
|
|
|
else:
|
|
|
- # subtitle_cmd = "drawtext=text='分享、转发给群友':fontsize=28:fontcolor=black:x=(w-text_w)/2:y=h-text_h-15"
|
|
|
- subtitle_cmd = "drawtext=text='分享、转发给群友':x=(w-text_w)/2:y=h-text_h-15:fontsize=28:fontcolor=black:fontfile=/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"
|
|
|
- # 背景色参数
|
|
|
- background_cmd = "drawbox=y=ih-65:color=yellow@1.0:width=iw:height=0:t=fill"
|
|
|
+ subtitle_cmd = f"subtitles={single_video_srt}:force_style='Fontsize=14,Fontname=wqy-zenhei,Outline=2,PrimaryColour=&H00FFFF,SecondaryColour=&H000000,Bold=1,MarginV=30'"
|
|
|
+ draw = f"{subtitle_cmd}"
|
|
|
# 多线程数
|
|
|
num_threads = 4
|
|
|
# 构建 FFmpeg 命令,生成视频
|
|
|
ffmpeg_cmd_oss = [
|
|
|
- "ffmpeg",
|
|
|
- "-f", "concat",
|
|
|
- "-safe", "0",
|
|
|
- "-i", f"{text_ptah}", # 视频文件列表
|
|
|
- "-i", audio_video, # 音频文件
|
|
|
- "-c:v", "libx264",
|
|
|
- "-c:a", "aac",
|
|
|
- "-threads", str(num_threads),
|
|
|
- "-vf", f"scale=320x480,{background_cmd},{subtitle_cmd}", # 添加背景色和字幕
|
|
|
- "-t", str(int(audio_duration)), # 保持与音频时长一致
|
|
|
- "-map", "0:v:0", # 映射第一个输入的视频流
|
|
|
- "-map", "1:a:0", # 映射第二个输入的音频流
|
|
|
- "-y", # 覆盖输出文件
|
|
|
- v_oss_path
|
|
|
+ "ffmpeg",
|
|
|
+ "-f", "concat",
|
|
|
+ "-safe", "0",
|
|
|
+ "-i", f"{single_video_txt}",
|
|
|
+ "-c:v", "libx264",
|
|
|
+ "-c:a", "aac",
|
|
|
+ "-threads", str(num_threads),
|
|
|
+ "-vf", f"scale=320x480,{draw}",
|
|
|
+ "-y",
|
|
|
+ single_video_url
|
|
|
+
|
|
|
]
|
|
|
- try:
|
|
|
- subprocess.run(ffmpeg_cmd_oss)
|
|
|
- print("视频处理完成!")
|
|
|
- if os.path.isfile(text_ptah):
|
|
|
- os.remove(text_ptah)
|
|
|
- except subprocess.CalledProcessError as e:
|
|
|
- print(f"视频处理失败:{e}")
|
|
|
- print(f"{mark}:视频拼接成功啦~~~")
|
|
|
- return video_files
|
|
|
-
|
|
|
- # 计算需要拼接的视频
|
|
|
- @classmethod
|
|
|
- def concat_videos_with_subtitles(cls, videos, audio_duration, platform, mark):
|
|
|
- # 计算视频文件列表总时长
|
|
|
- if platform == "baokuai":
|
|
|
- total_video_duration = sum(cls.get_video_duration(video_file) for video_file in videos)
|
|
|
- else:
|
|
|
- total_video_duration = sum(cls.get_video_duration(video_file[3]) for video_file in videos)
|
|
|
- if platform == "koubo" or platform == "zhannei" or platform == "baokuai":
|
|
|
- # 视频时长大于音频时长
|
|
|
- if total_video_duration > audio_duration:
|
|
|
- return videos
|
|
|
- # 计算音频秒数与视频秒数的比率,然后加一得到需要的视频数量
|
|
|
- video_audio_ratio = audio_duration / total_video_duration
|
|
|
- videos_needed = int(video_audio_ratio) + 2
|
|
|
- trimmed_video_list = videos * videos_needed
|
|
|
- return trimmed_video_list
|
|
|
- else:
|
|
|
- # 如果视频总时长小于音频时长,则不做拼接
|
|
|
- if total_video_duration < audio_duration:
|
|
|
- return ""
|
|
|
- # 如果视频总时长大于音频时长,则截断视频
|
|
|
- trimmed_video_list = []
|
|
|
- remaining_duration = audio_duration
|
|
|
- for video_file in videos:
|
|
|
- video_duration = cls.get_video_duration(video_file[3])
|
|
|
- if video_duration <= remaining_duration:
|
|
|
- # 如果视频时长小于或等于剩余时长,则将整个视频添加到列表中
|
|
|
- trimmed_video_list.append(video_file)
|
|
|
- remaining_duration -= video_duration
|
|
|
- else:
|
|
|
- trimmed_video_list.append(video_file)
|
|
|
- break
|
|
|
- return trimmed_video_list
|
|
|
+ subprocess.run(ffmpeg_cmd_oss)
|
|
|
+ return single_video_url
|
|
|
+
|
|
|
|
|
|
|
|
|
|