一键批量转换|压缩转码|整个文件夹的视频到新文件夹--基于python+ffmpeg实现

需求分析:

收到一个需求, 移动硬盘里面的视频素材需要剪辑, 但是格式是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的低码率的临时剪辑文件夹. 需求方很满意.

 

可扩展性:

  • 主要的参数外部化
  • 多线程
  • 异常处理
  • 日志记录
  • 多电脑部署渲染农场及之间的协调, 状态轮询
  • 渲染任务的最终报告, 异常统计
  • ...等等 自行封装吧

你可能感兴趣的:(python)