|
| 1 | +#!/usr/bin/env python |
| 2 | +# -*- encoding: utf-8 -*- |
| 3 | +""" |
| 4 | +@Description:ff_util.py ffmpeg截切命令工具 |
| 5 | +@Date :2022/03/14 |
| 6 | +@Author :xhunmon |
| 7 | + |
| 8 | +""" |
| 9 | +import os |
| 10 | +import re |
| 11 | +import time |
| 12 | +from datetime import datetime, timedelta |
| 13 | + |
| 14 | + |
| 15 | +def get_va_infos(ff_file, src): |
| 16 | + """ |
| 17 | + 获取视频的基本信息 |
| 18 | + @param ff_file: ffmpeg路径 |
| 19 | + @param src: 视频路径 |
| 20 | + @return: 结果:{'duration': '00:11:26.91', 'bitrate': '507', 'v_codec': 'h264', 'v_size': '1280x720', 'v_bitrate': '373', 'v_fps': '25', 'a_codec': 'aac', 'a_bitrate': '128'} |
| 21 | + """ |
| 22 | + cmd = r'{} -i "{}" -hide_banner 2>&1'.format(ff_file, src) |
| 23 | + output = os.popen(cmd).read() |
| 24 | + lines = output.splitlines() |
| 25 | + result = {} |
| 26 | + for line in lines: |
| 27 | + if line.strip().startswith('Duration:'): |
| 28 | + result['duration'] = line.split(',')[0].split(': ')[-1] |
| 29 | + result['bitrate'] = line.split(',')[-1].strip().split(': ')[-1].split(' ')[0] |
| 30 | + elif line.strip().startswith('Stream #0'): |
| 31 | + line = re.sub(r'\[.*?\]', '', re.sub(r'\(.*?\)', '', line)) |
| 32 | + if 'Video' in line: |
| 33 | + result['v_codec'] = line.split(',')[0].split(': ')[-1].strip() |
| 34 | + result['v_size'] = line.split(',')[2].strip().split(' ')[0].strip() |
| 35 | + result['v_bitrate'] = line.split(',')[3].strip().split(' ')[0].strip() |
| 36 | + result['v_fps'] = line.split(',')[4].strip().split(' ')[0].strip() |
| 37 | + elif 'Audio' in line: |
| 38 | + result['a_codec'] = line.split(',')[0].split(': ')[-1].strip() |
| 39 | + result['a_bitrate'] = line.split(',')[4].strip().split(' ')[0].strip() |
| 40 | + print(result) |
| 41 | + return result |
| 42 | + |
| 43 | + |
| 44 | +def get_duration(ff_file, src): |
| 45 | + ''' |
| 46 | + 执行命令获取输出这样的:Duration: 00:00:31.63, start: 0.000000, bitrate: 1376 kb/s |
| 47 | + :param ff_file: ffmpeg程序路径 |
| 48 | + :param src: 音视频文件 |
| 49 | + :return: 返回毫秒 |
| 50 | + ''' |
| 51 | + cmd = r'{} -i "{}" 2>&1 | grep "Duration"'.format(ff_file, src) |
| 52 | + info = os.popen(cmd).read() |
| 53 | + dur = info.split(',')[0].replace(' ', '').split(':') |
| 54 | + h, m, ss = int(dur[1]) * 60 * 60 * 1000, int(dur[2]) * 60 * 1000, dur[3] |
| 55 | + if '.' in ss: |
| 56 | + s1 = ss.split('.') |
| 57 | + s = int(s1[0]) * 1000 + int(s1[1]) * 10 |
| 58 | + else: |
| 59 | + s = int(ss) * 1000 |
| 60 | + return h + m + s |
| 61 | + |
| 62 | + |
| 63 | +def format_h_m_s(t): |
| 64 | + return f'0{t}' if t < 10 else f'{t}' |
| 65 | + |
| 66 | + |
| 67 | +def format_ms(t): |
| 68 | + if t < 10: |
| 69 | + return f'00{t}' |
| 70 | + return f'0{t}' if t < 100 else f'{t % 1000}' |
| 71 | + |
| 72 | + |
| 73 | +def format_to_time(ms): |
| 74 | + ''' |
| 75 | + 毫秒 --> 'xx:xx:xx.xxx' |
| 76 | + :param ms: 毫秒 |
| 77 | + :return: 'xx:xx:xx.xxx' |
| 78 | + ''' |
| 79 | + # t = timedelta(milliseconds=ms) |
| 80 | + # return str(t) |
| 81 | + h = format_h_m_s(int(ms / 1000 / 60 / 60)) |
| 82 | + m = format_h_m_s(int(ms / 1000 / 60 % 60)) |
| 83 | + s = format_h_m_s(int(ms / 1000 % 60)) |
| 84 | + m_s = format_ms(int(ms % 1000)) |
| 85 | + return '{}:{}:{}.{}'.format(h, m, s, m_s) |
| 86 | + |
| 87 | + |
| 88 | +def format_to_ms(duration: str): |
| 89 | + """ |
| 90 | + 'xx:xx:xx.xxx' --> 毫秒 |
| 91 | + @param duration: 时间长度'xx:xx:xx.xxx' |
| 92 | + @return: 毫秒 |
| 93 | + """ |
| 94 | + hms = duration.split(':') |
| 95 | + s_str = hms[2] |
| 96 | + ms_str = '0' |
| 97 | + if '.' in s_str: |
| 98 | + s_ms = s_str.split('.') |
| 99 | + s_str = s_ms[0] |
| 100 | + ms_str = s_ms[1] |
| 101 | + h = int(hms[0]) * 1000 * 60 * 60 |
| 102 | + m = int(hms[1]) * 1000 * 60 |
| 103 | + s = int(s_str) * 1000 |
| 104 | + ms = int(ms_str) |
| 105 | + return h + m + s + ms |
| 106 | + |
| 107 | + |
| 108 | +def srt_to_ass(ff_file, src, dst): |
| 109 | + os.system(f'{ff_file} -i {src} {dst}') |
| 110 | + |
| 111 | + |
| 112 | +def cut_with_subtitle(ff_file, src, dst, srt, width, height, margin_v, font_size=50, dur_full: str = None, |
| 113 | + start='00:00:00.000', tail='00:00:00.000', fps=None, |
| 114 | + v_bit=None, a_bit=None): |
| 115 | + """ |
| 116 | + 添加硬字幕:ffmpeg -i "../output/733316.mp4" -ss "00:02:00" -t 10 -r 23 -b:v 400K -c:v libx264 -b:a 38K -c:a aac -vf "subtitles=../output/input.ass:force_style='PlayResX=1280,PlayResY=720,MarginV=80,Fontsize=50'" ../output/ass.mp4 |
| 117 | + """ |
| 118 | + t_start = time.time() |
| 119 | + dur_ms = format_to_ms(dur_full) - format_to_ms(tail) - format_to_ms(start) |
| 120 | + dur = format_to_time(dur_ms) |
| 121 | + |
| 122 | + filename, ext = os.path.splitext(srt) |
| 123 | + ass = f'{filename}.ass' |
| 124 | + if os.path.exists(ass): |
| 125 | + os.remove(ass) |
| 126 | + ass_cmd = '{} -i "{}" "{}"'.format(ff_file, srt, ass) |
| 127 | + print(ass_cmd) |
| 128 | + os.system(ass_cmd) |
| 129 | + # ffmpeg -i "input.mp4" -ss "00:02:10.000" -t 12397 -r 15 -b:v 500K -c:v libx264 -c:a aac |
| 130 | + cmd = '{} -i "{}"'.format(ff_file, src) |
| 131 | + cmd = '{} -ss "{}" -t {}'.format(cmd, start, int(format_to_ms(dur) / 1000)) |
| 132 | + if fps: # 添加裁剪的fps |
| 133 | + cmd = '{} -r {}'.format(cmd, fps) |
| 134 | + # -c copy 不经过解码,会出现黑屏,因为有可能是P帧和B帧 |
| 135 | + if v_bit: # 添加视频bitrate,并且指定用libx264进行编码(ffmpeg必须安装) |
| 136 | + cmd = '{} -b:v {}K -c:v libx264'.format(cmd, v_bit) |
| 137 | + if a_bit: # 添加音频bitrate,并且指定用aac进行编码 |
| 138 | + cmd = '{} -b:a {}K -c:a aac'.format(cmd, a_bit) |
| 139 | + # -vf "subtitles=input.ass:force_style='PlayResX=1280,PlayResY=720,MarginV=70,Fontsize=50'" |
| 140 | + style = "PlayResX={},PlayResY={},MarginV={},Fontsize={}".format(width, height, margin_v, font_size) |
| 141 | + sub_file = "{}".format(ass) |
| 142 | + cmd = '''{} -vf "subtitles={}:force_style='{}'"'''.format(cmd, sub_file, style) |
| 143 | + # cmd = f'{cmd} -vf "subtitles={ass}:force_style="""PlayResX={width},PlayResY={height},MarginV={margin_v},Fontsize={font_size}""""' |
| 144 | + cmd = '{} {}'.format(cmd, dst) |
| 145 | + print(cmd) |
| 146 | + if os.path.exists(dst): |
| 147 | + os.remove(dst) |
| 148 | + os.system(cmd) |
| 149 | + os.remove(ass) |
| 150 | + print('一共花了 {} 秒 进行裁剪并添加字幕 {}'.format(int(time.time() - t_start), src)) |
| 151 | + |
| 152 | + |
| 153 | +def cut_va_full(ff_file, src, dst, dur: str = None, start='00:00:00.000', fps=None, v_bit=None, a_bit=None, |
| 154 | + copy_a=False): |
| 155 | + """ |
| 156 | + ffmpeg -i "input.mp4" -ss "00:02:10.000" -t 12397 -r 15 -b:v 500K -c:v libx264 -c:a aac "凡人修仙传1重制版-国创-高清独家在线观看-bilibili-哔哩哔哩.mp4" |
| 157 | + 其他所有的视频裁剪命令都需要通过这个实现 |
| 158 | + @param ff_file: ffmpeg路径 |
| 159 | + @param src: 输入路径 |
| 160 | + @param dst: 输出路径 |
| 161 | + @param dur: 裁剪长度,格式为'00:00:00.000' |
| 162 | + @param start: 裁剪的起点,如果dur=None,表示需要对时间进行裁剪,只是转换格式罢了 |
| 163 | + @param fps: 帧率,通常视频15~18帧即可,动漫一般24帧 |
| 164 | + @param v_bit: 单独控制视频的比特率 |
| 165 | + @param a_bit: 单独控制音频的比特率 |
| 166 | + @param copy_a: 直接复制音频通道数据,当 a_bit=None方有效 |
| 167 | + """ |
| 168 | + cmd = '{} -i "{}"'.format(ff_file, src) # 输入文件 |
| 169 | + if dur is not None: # 添加裁剪时间 |
| 170 | + cmd = '{} -ss "{}" -t {}'.format(cmd, start, int(format_to_ms(dur) / 1000)) |
| 171 | + if fps: # 添加裁剪的fps |
| 172 | + cmd = '{} -r {}'.format(cmd, fps) |
| 173 | + # -c copy 不经过解码,会出现黑屏,因为有可能是P帧和B帧 |
| 174 | + if v_bit: # 添加视频bitrate,并且指定用libx264进行编码(ffmpeg必须安装) |
| 175 | + cmd = '{} -b:v {}K -c:v libx264'.format(cmd, v_bit) |
| 176 | + if a_bit: # 添加音频bitrate,并且指定用aac进行编码 |
| 177 | + cmd = '{} -b:a {}K -c:a aac'.format(cmd, a_bit) |
| 178 | + elif copy_a: # 是否完全复制音频 |
| 179 | + cmd = '{} -c:a copy'.format(cmd) |
| 180 | + cmd = '{} "{}"'.format(cmd, dst) # 添加输出 |
| 181 | + if os.path.exists(dst): |
| 182 | + os.remove(dst) |
| 183 | + t_start = time.time() |
| 184 | + os.system(cmd) |
| 185 | + print('一共花了 {} 秒 进行裁剪 {}'.format(int(time.time() - t_start), src)) |
| 186 | + |
| 187 | + |
| 188 | +def cut_va_tail(ff_file, src, dst, dur_full: str = None, start='00:00:00.000', tail='00:00:00.000', fps=None, |
| 189 | + v_bit=None, a_bit=None, copy_a=False): |
| 190 | + """ |
| 191 | + 裁剪头尾 |
| 192 | + """ |
| 193 | + dur_ms = format_to_ms(dur_full) - format_to_ms(tail) - format_to_ms(start) |
| 194 | + dur = format_to_time(dur_ms) |
| 195 | + cut_va_full(ff_file, src, dst, dur, start, fps, v_bit, a_bit, copy_a) |
| 196 | + |
| 197 | + |
| 198 | +def cut_audio(ff_file, src, start, dur, dst): |
| 199 | + ''' |
| 200 | + 裁剪一段音频进行输出 |
| 201 | + :param ff_file: ffmpeg程序路径 |
| 202 | + :param src: 要裁剪的文件路径,可以是视频文件 |
| 203 | + :param start: 开始裁剪点,单位毫秒开始 |
| 204 | + :param dur: 裁剪时长,单位秒 |
| 205 | + :param dst: 输出路径,包括后缀名 |
| 206 | + :return: |
| 207 | + ''' |
| 208 | + if os.path.exists(dst): |
| 209 | + os.remove(dst) |
| 210 | + os.system( |
| 211 | + r'{} -i "{}" -vn -acodec copy -ss {} -t {} "{}"'.format(ff_file, src, format_to_time(start), dur, dst)) |
| 212 | + |
| 213 | + |
| 214 | +def cut_video(ff_file, src, start, dur, fps, bit, dst): |
| 215 | + ''' |
| 216 | + 裁剪一段视频进行输出, -ss xx:xx:xx.xxx |
| 217 | + :param ff_file: ffmpeg程序路径 |
| 218 | + :param src: 要裁剪的文件路径,可以是视频文件 |
| 219 | + :param start: 开始裁剪点,单位毫秒开始 |
| 220 | + :param dur: 裁剪时长,单位秒 |
| 221 | + :param fps: 帧率,通常是25~30 |
| 222 | + :param bit: 比特率,通常是1600~2000即可 |
| 223 | + :param dst: 输出路径,包括后缀名 |
| 224 | + :return: |
| 225 | + ''' |
| 226 | + if os.path.exists(dst): |
| 227 | + os.remove(dst) |
| 228 | + os.system( |
| 229 | + r'{} -i "{}" -ss {} -t {} -r {} -b:v {}K -an "{}"'.format(ff_file, src, format_to_time(start), dur, fps, |
| 230 | + bit, dst)) |
| 231 | + |
| 232 | + |
| 233 | +def cut_va_dur(ff_file, src, dst, start=0, dur=0, fps=None, bit=None): |
| 234 | + """ |
| 235 | + 根据头尾裁剪视频 |
| 236 | + :param ff_file: ffmpeg工具 |
| 237 | + :param src: 输入资源 |
| 238 | + :param dst: 输出文件 |
| 239 | + :param start: 起点 |
| 240 | + :param end: 终点 |
| 241 | + :param fps: 帧率 |
| 242 | + :param bit: 比特率 |
| 243 | + :return: |
| 244 | + """ |
| 245 | + length = get_duration(ff_file, src) |
| 246 | + if start + dur > length: |
| 247 | + print('裁剪比视频长') |
| 248 | + return |
| 249 | + if os.path.exists(dst): |
| 250 | + os.remove(dst) |
| 251 | + |
| 252 | + cmd = r'{} -i "{}"'.format(ff_file, src) |
| 253 | + cmd = cmd + ' -ss {}'.format(format_to_time(start)) |
| 254 | + cmd = cmd + ' -t {}'.format(format_to_time(dur)) |
| 255 | + if fps: |
| 256 | + cmd = cmd + ' -r {}'.format(fps) |
| 257 | + if bit: |
| 258 | + cmd = cmd + ' -b:v {}K'.format(bit) |
| 259 | + cmd = cmd + ' -c:v libx264 "{}"'.format(dst) # 使用x264解码后重新封装 |
| 260 | + # cmd = cmd+' -c copy {}'.format(dst) #不经过解码,会出现黑屏 |
| 261 | + os.system(cmd) |
| 262 | + |
| 263 | + |
| 264 | +def cut_va_start_end(ff_file, src, dst, start='00:00:00', end='00:00:00', dur='00:00:00', fps=None, bit=None): |
| 265 | + dur = format_to_ms(dur) - format_to_ms(end) |
| 266 | + cmd = f'{ff_file} -i "{src}" -ss {start} -t {dur} -c copy "{dst}"' |
| 267 | + if os.path.exists(dst): |
| 268 | + os.remove(dst) |
| 269 | + os.system(cmd) |
| 270 | + |
| 271 | + |
| 272 | +def cut_va_end(ff_file, src, dst, start=0, end=0, fps=None, bit=None): |
| 273 | + """ |
| 274 | + 根据头尾裁剪视频 |
| 275 | + :param ff_file: ffmpeg工具 |
| 276 | + :param src: 输入资源 |
| 277 | + :param dst: 输出文件 |
| 278 | + :param start: 起点 |
| 279 | + :param end: 终点 |
| 280 | + :param fps: 帧率 |
| 281 | + :param bit: 比特率 |
| 282 | + :return: |
| 283 | + """ |
| 284 | + length = get_duration(ff_file, src) |
| 285 | + dur = length - (start + end) |
| 286 | + if dur <= 0: |
| 287 | + print('裁剪比视频长') |
| 288 | + return |
| 289 | + cut_va_dur(ff_file, src, dst, start, dur, fps, bit) |
| 290 | + |
| 291 | + |
| 292 | +def muxer_va(ff_file, src_v, src_a, dst): |
| 293 | + if os.path.exists(dst): |
| 294 | + os.remove(dst) |
| 295 | + os.system(r'{} -i "{}" -i "{}" -c:v copy -c:a aac -strict experimental "{}"'.format(ff_file, src_v, src_a, dst)) |
0 commit comments