PyQt+moviepy音视频剪辑实战1:多个音视频合成顺序播放或同屏播放的视频文件实现详解

一、引言

在《moviepy音视频剪辑:音视频的加载和输出》、《moviepy音视频剪辑:多个视频合成一个视频》、《moviepy音视频剪辑:使用VideoFileClip、AudioFileClip和write_videofile、write_audiofile进行音视频的加载和输出》和《moviepy音视频剪辑:使用concatenate_videoclips和clips_array将多个视频合成一个顺序播放或同屏播放的视频》介绍了音视频文件加载和输出以及多视频合成一个视频的方法,本节将使用PyQt和moviepy结合开发一个音视频合成的GUI应用。

二、功能及界面设计

2.1、主界面

在这里插入图片描述

以mainwindow为基础设计窗口主界面,包含一个菜单和对应工具条,用于选择要合成的文件、去除选中的文件、合成参数配置和执行合成操作等功能。

本次对该界面的信号处理没有使用UI界面来定义信号和槽的关联,因为线条太多会不好修改,相关信号和槽的连接主要通过代码实现。

2.2、参数配置界面

在这里插入图片描述

根据选择的不同合成类型,可选配置不同的参数,也可以不配置,关于这些参数的说明请参考引言中提到的博文介绍。

2.3、输出信息窗

老猿为准备开发的视频工具提供了一个统一的输出信息窗,moviepy本身的输出信息将全部被接管到该输出信息窗显示。界面设计如图:


在这里插入图片描述

关于输出信息截获请参考《在Python实现print标准输出sys.stdout、stderr重定向及捕获的简单办法》以及《PyQt(Python+Qt)学习随笔:print标准输出sys.stdout以及stderr重定向QTextBrowser等图形界面对象》。

三、代码实现

3.1、主界面构造方法

class mainWin(QtWidgets.QMainWindow,ui_mixClips.Ui_ui_mainWin):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.initValues() #完成初始化成员变量
        self.initSignalAndSlots() #完成信号和槽的连接
        self.initPublicFrame()  #完成公共框架相关变量初始化

上面代码调用很简单,相关方法都好理解,只有initPublicFrame方法比较特殊,这是因为为了支持工具的开发只关注工具本身的功能,老猿单独开发了几个单独的模块用于所有工具都能使用,这些功能包括显示About窗口信息、截获标准输出、显示或关闭信息输出窗、信息输出窗与应用本身的QMainWindow对象关联(作为一个QDockWidget对象,关于QDockWidget请参考《第三十一章、containers容器类部件QDockWidget停靠窗功能介绍》或参考免费专栏《PyQt入门知识目录》相关章节的介绍)等功能,在此就不详细介绍了。

3.2、界面输入内容校验方法

    def validateAllInput(self,isOutputMessage=False):
        #效验所有文件是否都存在
        ret = True
        fileList = self.videoFileListModel.stringList()
        if fileList:
            count = len(fileList)
            if  count<2:
                self.actionProcessVideos.setEnabled(False)

                if isOutputMessage:print(f"输入视频文件数为{count},必须至少2个文件")
                ret = False
            else:
                for fileName in  fileList:
                    if len(fileName)==0:continue
                    if not os.path.exists(fileName):
                        if isOutputMessage:print(f"文件{fileName}不存在,请修订后再进行合成处理!")
                        ret = False
                if ret:
                    if not self.outputFileNameManuChanged:
                        filePre =  self.lastFileDir +"\\video_"+self.configW.composeType
                        self.outputFileName =  filePre + time.strftime("%Y%m%d%H%M%S", time.localtime()) + ".mp4"
                        self.input_outputFile.setText(self.outputFileName)
                        self.outputDir = self.lastFileDir
        else:
            ret = False
            if isOutputMessage:print(f"没有输入视频文件,必须至少2个文件")
            #print(self.videoFileListModel.stringList())

        if not self.outputDir:
            ret = False
            if isOutputMessage:print("输出文件没有指定")
        elif not os.path.exists(self.outputDir):
            ret = False
            if isOutputMessage:print(f"输出文件对应目录:{self.outputDir} 不存在")
        #self.btn_processVideoFiles.setEnabled(ret)
        self.actionProcessVideos.setEnabled(ret)
        if ret:
            if isOutputMessage:print("所有输入数据检测正常!")
        if self.configW.composeType!='stack' and self.configW.transitionFileName and len(self.configW.transitionFileName):
            if not os.path.exists(self.configW.transitionFileName):
                if isOutputMessage:print(f"转场文件{self.configW.transitionFileName}不存在,请修订后再进行合成处理!")
                ret = False
        return ret

该方法在所有界面内容输入发送变化后触发,用于检测输入内容是否完整、合法,如果返回False,则视频合成操作不能进行。该方法带的参数用于控制是否输出检测到的异常信息,当各组件正在输入时不应输出以免干扰,而最后要执行合成前会再校验一次,此次校验的异常则会输出。检测内容请见相关输出信息。

3.3、合成处理方法

该方法包含了三种合成方式处理的完整代码,有点长。

    def processFiles(self):
        print("\n\n合成处理开始......")

        if self.loadWin: self.loadWin.openCaptureWin() #打开输出信息窗口
        if not self.validateAllInput(True):return #检测有异常则终止合成
        tmpClip = [] #用于保存所有需要参与合成视频文件的剪辑对象
        try:
            fileList = self.videoFileListModel.stringList()  #取合成输入视频文件名列表
            fileCount = len(fileList)  
            for fileName in fileList:
                print(f"准备加载视频文件:{fileName} ")

                clip = mpe.VideoFileClip(fileName,verbose=True)
                print(f"加载视频文件:{fileName} 完成,时长为{clip.duration}秒,视频分辨率大小为:{clip.size} ")
                tmpClip.append(clip)
                print(f"视频文件:{fileName} 已经加载并缓存")

            transitionClip = None
            if self.configW.composeType != 'stack':#视频拼接可能需要转场文件
                if self.configW.transitionFileName and len(self.configW.transitionFileName):
                    print(f"准备加载转场文件:{self.configW.transitionFileName}")
                    transitionClip = mpe.VideoFileClip(self.configW.transitionFileName)
                    print(f"转场文件加载成功,时长为{transitionClip.duration}")


            print("进行内存视频合成...")
            padding = 0
            if   self.configW.composeType=='compose': #将所有输入剪辑全部统一分辨率方式合成则获取对应参数配置
                method = 'compose'
                bgcolor =  self.configW.bgColor
                padding = self.configW.input_padding.value()
                if padding==0.00:
                    padding = 0
                print("padding=", padding, 'bgcolor=', bgcolor, 'method=', method)
                destClip = mpe.concatenate_videoclips(tmpClip, method=method, padding=padding, bg_color=bgcolor,transition=transitionClip) #执行顺序拼接,统一分辨率

            elif self.configW.composeType=='chain': #保持所有输入视频分辨率不变进行视频拼接则获取对应参数配置
                padding = 0
                bgcolor = None
                method = 'chain'
                print("padding=", padding, 'bgcolor=', bgcolor, 'method=', method)
                destClip = mpe.concatenate_videoclips(tmpClip, method=method, padding=padding, bg_color=bgcolor,transition=transitionClip)#执行顺序拼接
            elif self.configW.composeType=='stack':#进行同屏播放合成则获取对应参数配置
                bgcolor = self.configW.bgColor
                #下面代码用于设置屏幕上视频的行数和列数
                if fileCount<=3: 
                    lines = 1
                    columns = fileCount
                elif fileCount<=10:
                    lines = 2
                    columns = int((fileCount+1)/2)
                else:
                    lines = 3
                    columns = int((fileCount+2)/3)

                print(f"视频将排列成{lines}行{columns}列")
                clipArrays = []
                tmpClipArray = []
                lines = column= 0
                for clip in tmpClip:#按行列将视频排列
                    tmpClipArray.append(clip)
                    column += 1
                    if column == columns:
                        clipArrays.append(tmpClipArray)
                        column = 0
                        tmpClipArray = []

                destClip = mpe.clips_array(clipArrays) #进行同屏播放合成

            print(f"内存视频合成完成,准备输出到文件:{self.outputFileName}.")
            destClip.write_videofile(self.outputFileName) 
            print(f"输出到文件:{self.outputFileName} 成功!")

        except Exception as e:
            print(f"进行视频处理合成失败,请参考上面输出信息确认处理存在问题的文件,异常原因:\n{e}")
            strinfo = str(e)
            if strinfo.find("codec can't decode"):
                print("该问题是由于视频文件解码导致的错误,请尝试将文件名或目录名改成纯ASCII字符集再尝试一下")

四、运行界面截图

4.1、加入合成文件后的主界面

在这里插入图片描述

可以看到支持重复加入视频,本案例就是将《笑看风云》这个视频重复四次进行合成。如果是拼接就是四个接连播放,如果是同屏播放则一个界面上播放四个视频。

4.2、设置为统一分辨率拼接合成

由于padding这个参数不能用于chain模式的拼接,因此为了展示效果,设置了padding参数为-1,表示前后两段视频有1秒的重叠。参数设置界面如下:

在这里插入图片描述

执行合成处理,下图为合成处理过程的一个截图:


在这里插入图片描述

合成处理挺快,但输出比较耗时间。
播放就是顺序播放,截图不能体现什么,但可以与同屏播放合成对比一下:


在这里插入图片描述

不好意思免费做广告了。

4.3、设置为同屏播放方式合成

主界面和运行界面与拼接没有什么区别,参数配置界面如下:


在这里插入图片描述

合成后的视频截图:

在这里插入图片描述

五、打包成exe

使用《PyQt(Python+Qt)学习随笔:windows下使用pyinstaller将PyQt文件打包成exe可执行文件》介绍的方法进行打包。

老猿在win7上最终打包的可执行程序包已经上传到百度云,大家可以下载下来长期免费使用。具体下载地址为百度网盘。

链接:https://pan.baidu.com/s/1UNaA2UqQBoxx-v8rCIPDhA

提取码:yh2d

选择该链接下的:视频合成工具.rar 即可。

广告

老猿关于PyQt的付费专栏《使用PyQt开发图形界面Python应用》只需要9.9元,本专栏《PyQt+moviepy音视频剪辑实战》文档的同样内容在付费专栏上也有相应内容,总体来说付费专栏介绍更详细或案例更多。本节内容对应付费专栏的《PyQt+moviepy音视频剪辑实战1:多视频合成顺序播放或同屏播放的视频文件》。如果有兴趣也愿意支持老猿的读者,欢迎购买付费专栏。

<a href="https://blog.csdn.net/LaoYuanPython/article/details/98245036"><img src="https://img-blog.csdnimg.cn/20190426190559122.png" ><img src="https://img-blog.csdnimg.cn/20200422115441574.png" ></a>

跟老猿学Python、学5G!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342