需求分析:
收到一个需求, 移动硬盘里面的视频素材需要剪辑, 但是格式是sony A9录制的4K视频, 对剪辑硬件要求很高, 所以要压缩码率以便实现流畅剪辑的体验. 虽然有proxy这种方式但是常用的人都知道有各种小毛病. 那么这里的需求是preview剪辑完之后再把源视频文件及直接覆盖回去( premiere直接一键relocate )就可以渲染高清视频了.同时也能节约硬盘空间.
那么, 一般常规做法是整理素材,分类,然后使用Adobe Media Encoder批量压缩. 虽然 Adobe Encoder 有 media pool 这种东西但是无法批量转换整个文件夹并且还保持目录结构不变. 然后我有另外网上搜了一圈试用了好几个软件, 要么收费, 要么就是无法保持文件夹结构. 后来一想, 似乎可以自己写一个简单的程序来辅助处理.
实现思路:
- - 运行环境: windows系统/mac/linux都可以
- - 需要自己先安装组件 ffmpeg ( https://ffmpeg.org/download.html ) 和 python3.x ( https://www.python.org/downloads/ ) 这两个是全平台都有的, 自行下载对应平台安装即可, 这里我下载的是windows版本的.
- - 实现思路: 利用 os.path 之类的遍历源文件夹和子目录, 复制到新的目标文件夹里面, 复制的过程中如果检测到是 mp4 文件(或者mov等等) 则开始自动转码, 因为转码本身就是多线程另外遍历文件夹的开销相对可以忽略不计, 所以 python 主进程不再实现多进程也不实现异常处理, 需要的可以自行改动代码.
实现目标:
- 要求克隆整个文件夹和子目录文件夹结构不变, 包括各种文件类型, 但只转码其中的所有视频文件.
- 要求转码完成结束的时候可选自动关机.
- 要求结束的时候出一个转换报告.
- 中途如果出错或者需要暂停, 能够"断点续转"
- 如果导入时候个别文件出错, 那么删掉问题文件, 再启动转码工具, 在默认断点续传开启的前提下, 会自动单独转码问题文件
- 支持非ASCII文件名和文件夹目录
运行示例:
C:\python.exe bathconvert.py
参数需要编辑 bathconvert.py 在源代码末尾的主程序入口设定.
源代码:
import os
import shutil
import subprocess
import datetime as datetime
from colorama import Fore
import json
class MaoFolderConvert:
__support_formats = ['.mp4'] # need all be lowered string, default .mp4
__resume = False
__startfile = ''
__endfile = ''
counter = 0
total_files = 0
pars = []
__cachefile = r"C:\mao_cache.json" # 交换临时文件, 不需要更改, 用于记录当前编码位置, 用于"断点续传"
def __init__(self, formats, pars, resume):
self.__support_formats = formats
self.pars = pars
self.__resume = resume
# 设置当前正在处理,未完成的文件
def setindex(self, index):
with open(self.__cachefile, 'w') as f:
f.write(json.dumps(index, ensure_ascii=False))
f.close()
# 获取当前正在处理,未完成的文件
def getindex(self):
if not os.path.exists(self.__cachefile):
with open(self.__cachefile, 'w') as f:
f.write(json.dumps({'src': '', 'des': ''}, ensure_ascii=False))
f.close()
return ''
with open(self.__cachefile, 'r') as f:
res = json.load(f)
f.close()
return res
def convert(self, src, des):
ffmpegCmd = 'ffmpeg -i "{}" -vcodec libx264 -b:v {} -vf scale={}:-2 -threads 4 "{}" -y '.format(src,
self.pars[0],
self.pars[1],
des)
self.setindex({'src': src, 'des': des})
returncode = subprocess.call(ffmpegCmd)
print(Fore.LIGHTGREEN_EX + '[ ' + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ' ] Done! => ' + des)
self.counter = self.counter + 1
self.setindex({'src': '', 'des': ''})
def start(self, path, out):
lastfile = self.getindex()
if lastfile is not '' and lastfile['src'] is not '': # 首先处理未完成任务
self.convert(lastfile['src'], lastfile['des'])
for files in os.listdir(path):
name = os.path.join(path, files)
back_name = os.path.join(out, files)
if os.path.isfile(name):
basedir = (os.path.abspath(os.path.dirname(back_name)))
if not os.path.isdir(basedir):
os.makedirs(basedir)
if os.path.splitext(name)[-1].lower() in self.__support_formats:
if not os.path.exists(back_name) or not self.__resume:
self.convert(name, back_name) # only convert what we need
self.total_files = self.total_files + 1
else:
if not os.path.exists(back_name) or not self.__resume:
shutil.copy(name, back_name) # copy other files directly
self.total_files = self.total_files + 1
else:
if not os.path.isdir(back_name):
os.makedirs(back_name)
self.start(name, back_name)
if __name__ == '__main__':
A = r"J:\@CLIENT_RAW\xx" # 源文件夹
B = r"J:\temp\xx" # 目标文件夹指定(自动创建,如果存在则强制覆盖)
autodown = False # 转换完之后是否自动关机,默认不关机
resume = True # 是否断点继续压缩(不从头覆盖), 默认是断点续写模式
pars = [
'3000k', # 要压缩为的目标视频码率
'1280' # 目标视频宽度, 高度自适应
]
starttime = datetime.datetime.now()
print("===== Mao Convert Start =====")
mao = MaoFolderConvert(['.mp4'], pars, resume)
mao.start(A, B)
duration = (datetime.datetime.now() - starttime).seconds
m, s = divmod(duration, 60)
h, m = divmod(m, 60)
duration = "%d:%02d:%02d" % (h, m, s)
print("===== Done! Convert Report =====")
print(
"== Converted video: {}, total files: {}\r\n== Total time: {} ".format(mao.counter,
mao.total_files,
duration))
if autodown:
os.system('shutdown -s -t 1')
实测240G文件夹压缩为14G的低码率的临时剪辑文件夹. 需求方很满意.
可扩展性:
- 主要的参数外部化
- 多线程
- 异常处理
- 日志记录
- 多电脑部署渲染农场及之间的协调, 状态轮询
- 渲染任务的最终报告, 异常统计
- ...等等 自行封装吧