• PyQt+moviepy音视频剪辑实战2:一个剪裁视频文件精华内容留存工具的实现


    一、引言

    最近网上会议很多,网上会议工具大多提供了录播的功能,有些会议内容比较精彩,但中间穿插有些无用的内容,或者有些只有几段精彩,大部分内容可以去除。这就需要对该录播文件进行剪辑,取其精华留存,这样可以节约后续重温或者给其他人共享的时间。本文介绍的开发方法就是要实现这样的一个工具。

    二、背景知识介绍

    2.1、视频的读取和输出保存

    本部分知识请参考《moviepy音视频剪辑:音视频的加载和输出》或专栏《PyQt+moviepy音视频剪辑实战》相关文章即可。

    2.2、视频的截取

    视频的截取使用subclip方法,该方法为clip类的方法,moviepy中clip类是所有剪辑的基类。

    语法如下: subclip(self, t_start=0, t_end=None)

    2.3、视频的拼接

    本节的案例是从同一个视频取几段顺序拼接合成,这些段的分辨率相同,因此可以用保持分辨率拼接和统一分辨率拼接都可以,相关知识请参考《moviepy音视频剪辑:多个视频合成一个视频》或专栏《PyQt+moviepy音视频剪辑实战》相关文章即可。

    三、图形界面设计

    本程序除了主界面之外的部分都是复用《PyQt+moviepy音视频剪辑实战1:多个音视频合成顺序播放或同屏播放的视频文件实现详解》、《PyQt+moviepy音视频剪辑实战1:多视频合成顺序播放或同屏播放的视频文件》的公用框架。

    主界面如下:
    在这里插入图片描述
    该界面的不同部分留了过多的空间,这是为了要在底部动态构建一个停靠窗部件用于显示输出信息使用。

    四、代码实现

    4.1、主界面类及构造方法

    class mainWin(QtWidgets.QMainWindow,ui_multiSegmentClip.Ui_MainWindow):
        def __init__(self):
            super().__init__()
            self.setupUi(self)
            self.initValues()
            self.initSignalAndSlots()
            self.initPublicFrame()
    
    

    4.2、槽和信号连接方法initSignalAndSlots

        def initSignalAndSlots(self):
            self.btn_choiceSrc.clicked.connect(self.chooseFile)
    
            self.videoFile.textChanged['QString'].connect(self.fileNameInputed)
            self.btn_choiceDest.clicked.connect(self.chooseFile)
            self.startPos.editingFinished.connect(self.getDestFName)
            self.endPos.editingFinished.connect(self.getDestFName)
            self.actionmergeClips.triggered.connect(self.convert)
            self.actionridClips.triggered.connect(self.convert)
    

    4.3、视频文件、输出文件手工输入或选择方法

     def fileNameInputed(self,fname=None): #源视频文件手工输入编辑完成后调用本方法
            ret = False
            if not fname or fname==True:fname = self.videoFile.text()
            if self.srcDir:
                dir = QtCore.QDir(self.srcDir)
                ret = dir.exists(fname)
            self.actionmergeClips.setEnabled(ret)
            self.actionridClips.setEnabled(ret)
            self.getDestFName()
    
        def chooseFile(self): #点击输出文件选择或视频文件选择调用本槽方法用于选择文件
            btnName = self.sender().objectName()
    
            if btnName == 'btn_choiceSrc':
    
                if self.srcDir:fname = self.srcDir
                else:fname = ""
                fileName = self.fileDialog.getOpenFileName(self, "选择视频文件",fname, "video Files (*.mp4)")# *.wmv *.rm *.avi *.flv *.webm *.wav *rmvb )")
                if fileName[0]=='':return
                fileName = QtCore.QDir.toNativeSeparators(fileName[0])
                self.videoFile.setText(fileName)
                self.fileNameInputed(fileName)
            else:
                if self.destDir: fname = self.destDir
                else:  fname = r""
                fileName = self.fileDialog.getSaveFileName(self, "选择要保存的视频存储文件",fname,"video Files (*.mp4)")# *.wmv *.rm *.avi *.flv *.webm *.wav *rmvb)")
                if fileName[0] == '': return
                fileName = QtCore.QDir.toNativeSeparators(fileName[0])
                self.saveFile.setText(fileName)
                destDir = fileName.rsplit('\',1)[0]
                self.destDir = destDir
                print(self.destDir)
    
        def getDestFName(self): #根据视频文件和视频剪辑时间段设置自动生成一个输出文件名
            srcFile = self.videoFile.text()
            if not srcFile:return
            file_pre, file_type = srcFile.split('.')
            if not file_type: return
    
            ##计算文件名长度是否小于255
            lenPre = len(file_pre)
            segStart = self.startPos.text().strip(" 
    ")
            segEnd = self.endPos.text().strip(" 
    ")
            lenSegStart = len(segStart)
            lenSegEnd = len(segEnd)
            if (lenPre + lenSegStart + lenSegEnd) > 240:
                lenSeg = (240 - lenPre) / 2
                segStart = segStart[0:lenSeg]
                segEnd = segEnd[0:lenSeg]
            else:
                segStart = segStart[0:]
                segEnd = segEnd[0:]
            self.videoFName = file_pre +'_'+segStart+ '_' + segEnd + '.'+file_type
    
            self.videoFName = self.videoFName.replace(',','_')
            destDir = file_pre.rsplit('\', 1)[0]
            self.srcDir = destDir
            self.destDir = destDir
            self.saveFile.setText(self.videoFName)
    

    4.4、视频拼接处理方法

        def convertByMoviepy(self,srcFile,destFile,isMergeClip)://执行视频拼接处理
            paths = destFile.rsplit('\',1)
            if len(paths)==2:
                path = paths[0]
                fname = paths[1]
            else:
                fname = destFile
                path = ''
            if isMergeClip:
                fname = 'merge_'+fname
            else:fname = 'rid_'+fname
            if path=='':destFile = fname
            else:destFile = path+'\'+fname
            print("执行视频提取开始,源文件:",srcFile,' --> 目标文件:',destFile)
            start = time.clock()
            print(start)
            try:
                validClipDistance = self.validateSlipDistance(isMergeClip)
                if not validClipDistance:return
    
                videoFile = mpe.VideoFileClip(srcFile)
                print(f"视频总长:{videoFile.duration}秒")
                self.destClip = None
                destClip = None
    
                for dist in validClipDistance:
                    destClip = self.mergeClip(videoFile,dist)
                print("开始写目标文件.")
                destClip.write_videofile(destFile)
                print("目标文件生成完成")
                videoFile.close()
                destClip.close()
                print("执行视频提取成功,保存在文件:", destFile)
            except Exception as e:
                info = f"视频文件无法读取,可能是因为格式不支持:{e}"
                print(info)
                print("任务无法执行!")
            finally:
                print("处理耗时(秒):",time.clock()-start)
    
        def mergeClip(self,clip,distance):#从clip取distance指定的视频段合并到输出剪辑 self.destClip
            start, end = distance
            try:
                duration = int(clip.duration)
    
                if end>duration:end = duration
                if start>duration:start = duration
                if not end:end = duration
                subClip = clip.subclip(start,end)
                if self.destClip:
                    self.destClip = mpe.concatenate_videoclips([self.destClip, subClip])
                else: self.destClip = subClip
            except Exception as e:
                print(f"合并片段:{start}--{end}失败,原因:{e}")
                return None
            else:
                print(f"合并片段:{start}--{end}成功")
                return self.destClip
    
    

    五、运行界面

    5.1、初始主界面

    在这里插入图片描述
    主界面上可以选择视频源文件、设定视频段,不过视频段的设置比较简陋,所有开始位置用英文逗号分隔放在“视频段开始位置”后面的编辑框中,结束位置放在“视频段结束位置”,两者数字和逗号都是纯ASCII半角字符,且二者的数字和逗号个数相等,且必须从低到高排列、除最后一个外结束位置必须大于开始位置,如果结束位置为0,则表示到视频最后。

    5.2、进行视频裁剪的运行过程界面

    在这里插入图片描述
    这是从F:video顺流逆流.mp4取0-3秒和20-25秒两段视频合并成一个视频输出。如果是指定视频段输出,输出文件名以merge开头,否则以rid开头。

    六、打包成windows执行文件

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

    老猿前不久用该工具实现了对一个长达150多分账的视频会议录播视频的23处精华内容进行了剪裁合并,最终生成文件为43分钟。不过在处理前要观看视频确认需要留存内容。

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

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

    提取码:yh2d

    选择该链接下的:视频剪裁工具1.0.rar 即可。

    注意:

    百度云上分享的《咖啡狗免费工具软件共享空间》下的不同软件安装时必须解压到不同目录,如果解压到同一目录可能有冲突导致不能正常运行,
    但解压后遵循如下要求可以将其聚合到同一个目录:

    1. 放置到同一目录的不同软件的版本必须相同,版本为压缩文件名中VX.X标注;
    2. 聚合拷贝时除拷贝执行文件外,还有resource目录必须拷贝,如果resource目录下有相同文件名可以覆盖;
    3. 聚合拷贝exe文件和resource目录及其下文件到其他已解压工具目录后,源目录可以删除。

    更多moviepy的介绍请参考《PyQt+moviepy音视频剪辑实战文章目录》或《moviepy音视频开发专栏》。

    关于收费专栏

    老猿的付费专栏《使用PyQt开发图形界面Python应用》专门介绍基于Python的PyQt图形界面开发基础教程,付费专栏《moviepy音视频开发专栏》详细介绍moviepy音视频剪辑合成处理的类相关方法及使用相关方法进行相关剪辑合成场景的处理,两个专栏加起来只需要19.9元,都适合有一定Python基础但无相关专利知识的小白读者学习。这2个收费专栏都有对应免费专栏,只是收费专栏的文章介绍更具体、内容更深入、案例更多。

    收费专栏文章目录:《moviepy音视频开发专栏文章目录》、《使用PyQt开发图形界面Python应用专栏目录》,本文收费专栏对应文章为《PyQt+moviepy音视频剪辑实战2:实现一个剪裁视频文件精华内容留存工具》。

    对于缺乏Python基础的同仁,可以通过老猿的免费专栏《专栏:Python基础教程目录》从零开始学习Python。

    如果有兴趣也愿意支持老猿的读者,欢迎购买付费专栏。

    跟老猿学Python、学5G!

  • 相关阅读:
    重构之重新组织函数(Split Temporary Variable)
    HammperSpoon 不能 Focus Google Chrome 的问题
    如何让 vim 可以在命令行执行命令并且附加参数
    This bison version is not supported for regeneration of the Zend/PHP parsers
    php cURL library is not loaded
    aws linuxbrew GLIBC_PRIVATE not defined in file ld-linux-x86-64.so.2
    gen-cpp/.deps/ChildService.Plo: No such file or directory
    快速解码base64和utf-8的ASCII编码和URL解码
    英文版firefox显示中文字体丑的问题
    linux find 反转 查找没有被找到的结果
  • 原文地址:https://www.cnblogs.com/LaoYuanPython/p/13643515.html
Copyright © 2020-2023  润新知