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