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:多視頻合成順序播放或同屏播放的視頻文件》。如果有興趣也願意支持老猿的讀者,歡迎購買付費專欄。

跟老猿學Python、學5G!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章