Back
Featured image of post 【Programming】FFmpeg+Python GUI--Application for the rapid trimming and concatenation of video

【Programming】FFmpeg+Python GUI--Application for the rapid trimming and concatenation of video

This project aims to develop a user-friendly Graphical User Interface (GUI) application for the rapid trimming and concatenation of video intros and outros. Integrating a Qt Designer-built interface with the Python programming language and leveraging the powerful ffmpeg tool, users can effortlessly specify video files, set cut points, and execute video processing tasks. The ultimate goal is to enhance video editing efficiency, particularly catering to users who require batch video processing.

Github:VideoExtractAndConcat

Project Overview

This project aims to develop a user-friendly Graphical User Interface (GUI) application for the rapid trimming and concatenation of video intros and outros. Integrating a Qt Designer-built interface with the Python programming language and leveraging the powerful ffmpeg tool, users can effortlessly specify video files, set cut points, and execute video processing tasks. The ultimate goal is to enhance video editing efficiency, particularly catering to users who require batch video processing.

Brief Workflow Outline:

Environment Setup

Install necessary software and libraries: Ensure Python environment is properly configured, install PySide6, and ffmpeg.

Interface Design (Using Qt Designer)

Main Interface Design

Design includes buttons for file selection (to choose video files), input fields for time trimming (for intro and outro removal times or direct cropping period), output path selection button, preview window (optional), start processing button, and progress bar. Convert the .ui file into .py.

Writing Core Logic Code

  • Read Video Information: Use ffmpeg to fetch video duration and other basic information.
  • Time Processing: Based on user inputs, compute actual trimming command parameters (primarily end time).
  • Invoke ffmpeg: Construct and execute ffmpeg command-line instructions for video slicing and joining.
  • Progress Feedback: Implement logic to update progress bars, showcasing video processing advancement (may involve parsing ffmpeg output).
  • Error Handling: Catch and handle errors during ffmpeg execution, providing user-friendly alerts.

Binding Interface with Logic Code

Utilize PySide’s signals and slots mechanism to connect interface elements (e.g., button click events) to backend processing logic.

Testing & Debugging

Thoroughly test the software for accurate video trimming and concatenation, responsive UI, and effective error handling mechanisms.

Optimization & Aesthetics

Adjust layout based on test feedback for an improved user experience; consider incorporating multithreading to enhance program responsiveness, especially when dealing with large files.

Packaging & Deployment

Package the application into an executable file (using tools like PyInstaller) for easy distribution and deployment, ensuring it runs smoothly on systems without a development environment.

Interface Design

Employ Qt Designer for interface design, including creating UI elements, layout adjustments, and setting component properties.

pyside6-uic input.ui -o output.py

From main.py import ui

from PyQt5.QtWidgets import QApplication
from output import Ui_MainWindow  # 假设转换后生成的文件名为output.py

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self):
       super().__init__()
        self.setupUi(self)  # 初始化界面
        # 进一步设置信号槽和其他逻辑...

if "__name__" == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec_()

Core Logic Code Implementation

Setting FFmpeg Path (in config.py)

import os

class ffpath:
    # 相对路径
    ffmpeg_path_relative = '.\\FFmpeg\\bin\\ffmpeg.exe'
    ffprobe_path_relative = '.\\FFmpeg\\bin\\ffprobe.exe'

    # 转换为绝对路径
    ffmpeg_path = os.path.abspath(ffmpeg_path_relative)
    ffprobe_path = os.path.abspath(ffprobe_path_relative)

Initializing FFmpeg (in ffmpegApi.py)

import subprocess
import os
#import time
# 从config.py中拿到ffmpeg.exe和ffprobe.exe的绝对路径
from config import ffpath

class FFmpeg:

    # 初始化函数,用于初始化实例的ffmpeg_path属性
    def __init__(self, ffmpeg_path):
        self.ffmpeg_path = ffmpeg_path

Defining run Method to Invoke cmd (in ffmpegApi.py)

While subprocess.Popen accepts a list, due to potential spaces and paths in FFmpeg commands that could lead to errors, we input as a string for reliability.

    # 定义run方法来执行FFmpeg命令
    def run(self, cmd):
        """
        执行给定的FFmpeg命令,并返回其输出。

        参数:
        - cmd: 一个列表,包含要执行的FFmpeg命令及其参数。

        返回值:
        - 执行命令的标准输出(字符串)。

        抛出:
        - Exception: 如果命令执行失败(返回码非0),则抛出包含错误信息的异常。
        """
        cmd = [self.ffmpeg_path] + cmd
        cmd_str = ' '.join(cmd)
        print(f"尝试执行:{cmd_str}")
        p = subprocess.Popen(cmd_str, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        out, err = p.communicate()
        if p.returncode != 0:
            print(f"命令执行失败,错误信息:{err.decode('utf-8')}")
            raise Exception(err.decode('utf-8'))

Video Trimming (in ffmpegApi.py)

definition of extract_video function:

def extract_video(self, input_folder, start_time, end, output_folder, encoder='-c:v copy -c:a copy'):
    """
    获得FFmpeg命令的逻辑,给出具体的FFmpeg命令并传入run函数中运行
    
    参数:
    folder:输入输出文件夹参数须为绝对路径字符串
    start_time, end:片头片尾的持续时间参数须为H:mm:ss:fff格式的字符串
    encoder参数须为字符串,默认为'-c:v copy -c:a copy'复制流
    """

Under extract_video, define time_calculate to handle tail durations, given FFmpeg’s inherent start-to-end trimming approach, requiring timestamp calculation based on the specified outro duration.

        def time_calculate(duration, end):
            # 转换为浮点数进行计算
            hours, minutes, seconds_milliseconds = end.split(':')
            seconds, milliseconds = seconds_milliseconds.split('.')
            hours = float(hours)
            minutes = float(minutes)
            end_float = hours * 3600 + minutes * 60 + float(seconds)
            end_float += float(milliseconds) / 1000
            end_time_float = duration - end_float
            print("结束时间点为:", end_time_float)
            # 浮点数结果转换为字符串格式
            m, s = divmod(end_time_float, 60)
            h, m = divmod(m, 60)
            end_time = "%02d:%02d:%06.3f" % (h, m, s)
            print("结束时间点为:", end_time)
            return end_time

Core in the extract_video function is using -accurate_seek, which, while not perfectly accurate, avoids errors, suitable for bulk operations. For precise single video edits, consider tools like LosslessCut.

        # 遍历文件夹中的所有mp4视频文件
        for file in os.listdir(input_folder):
            if file.endswith('.mp4'):
                input_file = os.path.join(input_folder, file)
                # 检测输出文件夹是否存在,不存在则创建
                if not os.path.exists(output_folder):
                    os.makedirs(output_folder)
                output_file = os.path.join(output_folder, file)
                # 读取视频的总时长(调用config.py中ffpath类的ffprobe_path),传入run函数中运行
                cmd1 = [ffpath.ffprobe_path, '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', input_file]
                print("执行:" + ' '.join(cmd1))
                result = subprocess.run(cmd1, capture_output=True, text=True)
                duration = float(result.stdout.strip())
                print("视频总秒数为:", duration)
                # 调用time_calculate函数将end时间转换为秒数浮点数计算后返回结束时间字符串
                end_time = time_calculate(duration, end)
                # 调用ffmpeg命令行工具,对视频进行截取
                cmd = ['-ss', start_time, '-to', end_time, '-accurate_seek', '-i', f'"{input_file}"',  encoder, f'"{output_file}"']
                # 打印最终输入命令行的cmd指令,从列表转换为字符串
                # print("执行:" + r'Q:\Git\FFmpeg-python\02FFmpegTest\FFmpeg\bin\ffmpeg.exe ' + ' '.join(cmd))
                self.run(cmd)
                print(file + '视频截取完成')
            else:
                print(file + '不是mp4文件,跳过')

Merging Videos (in ffmpegApi.py)

Define merge_video function, where the heart lies in the -filter_complex flag for unifying video formats pre-encoding. Time-consuming but reliable, it’s aimed at error prevention in batch processing. Based on practical experiences, standardizing frame rate, resolution, and pixel aspect ratio across videos is crucial.

def merge_video(self, input_folder, input_file1, input_file2, output_folder, encoder='-c:v libx264 -preset veryfast -crf 23 -c:a aac -b:a 192k -ar 44100 -ac 2'):
     """
    获得FFmpeg命令的逻辑,给出具体的FFmpeg命令并传入run函数中运行
    
    参数:
    folder:输入输出文件夹参数须为绝对路径字符串
    input_file:片头片尾参数须为绝对路径字符串
    encoder参数须为字符串,默认为'-c:v libx264 -preset veryfast -crf 23 -c:a aac -b:a 192k -ar 44100 -ac 2'重新编码
    """
    
        # 遍历文件夹中的所有mp4视频文件
        for file in os.listdir(input_folder):
            if file.endswith('.mp4'):
                input_file = os.path.join(input_folder, file)
                # 检测输出文件夹是否存在,不存在则创建
                if not os.path.exists(output_folder):
                    os.makedirs(output_folder)
                output_file = os.path.join(output_folder, file)
                # 调用ffmpeg命令行工具,对视频进行合并
                cmd = [
                    '-i', f'"{input_file1}"', 
                    '-i', f'"{input_file}"', 
                    '-i', f'"{input_file2}"', 
                    '-filter_complex', 
                    '"[0:v]fps=30,scale=1280:720,setsar=1[v0];[1:v]fps=30,scale=1280:720,setsar=1[v1];[2:v]fps=30,scale=1280:720,setsar=1[v2];[0:a]aformat=sample_rates=44100:channel_layouts=stereo[a0];[1:a]aformat=sample_rates=44100:channel_layouts=stereo[a1];[2:a]aformat=sample_rates=44100:channel_layouts=stereo[a2];[v0][a0][v1][a1][v2][a2]concat=n=3:v=1:a=1[vout][aout]" -map "[vout]" -map "[aout]"', encoder, f'"{output_file}"']
                # 打印最终输入命令行的cmd指令,从列表转换为字符串
                # print("执行:" + r'Q:\Git\FFmpeg-python\02FFmpegTest\FFmpeg\bin\ffmpeg.exe ' + ' '.join(cmd))
                self.run(cmd)
                print(file + '视频合并完成')
            else:
                print(file + '不是mp4文件,跳过')

Debugging Code (in ffmpegApi.py)

# 调用extract_video函数,对视频进行截取
input_folder = r'Q:\Git\FFmpeg-python\02FFmpegTest\input'
output_folder = r'Q:\Git\FFmpeg-python\02FFmpegTest\output1'
start_time = '00:00:01.000'
end = '00:00:03.500'
ffmpeg.extract_video(input_folder, start_time, end, output_folder)
print('视频截取完成')

# 调用merge_video函数,对视频进行合并
input_folder = r'Q:\Git\FFmpeg-python\02FFmpegTest\output1'
output_folder = r'Q:\Git\FFmpeg-python\02FFmpegTest\output2'
input_file1 = r'Q:\Git\FFmpeg-python\02FFmpegTest\input\1\op.mp4'
input_file2 = r'Q:\Git\FFmpeg-python\02FFmpegTest\input\1\ed.mp4'
ffmpeg.merge_video(input_folder, input_file1, input_file2, output_folder)
print('视频合并完成')

Pyside UI & Logic Binding (in main.py)

Import Third-party Libraries and Custom Python Libraries

from PySide6.QtWidgets import QApplication, QMainWindow, QFileDialog
from Ui_VideoEditor import Ui_MainWindow
from ffmpegApi import FFmpeg
from config import ffpath

# 导入ffmpeg路径
init1 = print("初始化ffmpeg路径为:", ffpath.ffmpeg_path)
init2 = print("初始化ffprobe路径为:", ffpath.ffprobe_path)

Class of MainWindow

class MainWindow(QMainWindow, Ui_MainWindow):
    # 初始化窗口
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.bind()
        # 打开控制台窗口
        # TODO:未完成

definition of bind function

    # 绑定事件槽
    def bind(self):
        # 设置按钮的信号槽
        self.importBn1.clicked.connect(self.import_video_folder1)
        self.importBn2.clicked.connect(self.import_video_file1)
        self.importBn3.clicked.connect(self.import_video_file2)
        self.importBn4.clicked.connect(self.import_video_folder2)
        self.exportBn1.clicked.connect(self.export_video_folder1)
        self.exportBn2.clicked.connect(self.export_video_folder2)
        self.pushButton1.clicked.connect(self.process_extract)
        self.pushButton2.clicked.connect(self.process_concat)
        self.pushButtonF.clicked.connect(self.adjust_ffmpeg_path)

adjust_ffmpeg_path function

    def adjust_ffmpeg_path(self):
        ffmpeg_folder = QFileDialog.getExistingDirectory(
            self, "选择bin文件夹", "./")
        ffpath.ffmpeg_path = f"{ffmpeg_folder}\\ffmpeg.exe"
        ffpath.ffprobe_path = f"{ffmpeg_folder}\\ffprobe.exe"
        if ffpath.ffmpeg_path:
            self.textEdit.append(f"ffmpeg路径修改为:{ffpath.ffmpeg_path};{ffpath.ffprobe_path}")
            print(ffpath.ffmpeg_path)

extract_video function

    # 切割流程
    # 点击导入视频文件夹按钮,弹出文件选择对话框,选择视频文件夹,选择完成后显示在文本框中
    def import_video_folder1(self):
        self.folder_path1 = QFileDialog.getExistingDirectory(
            self, "选择视频文件夹", "./")
        if self.folder_path1:
            self.textEdit.append(f"切割:输入文件夹为{self.folder_path1}")
    # 点击导出切割文件夹按钮,弹出文件选择对话框,选择文件夹,选择完成后显示在文本框中
    def export_video_folder1(self):
        self.folder1_path = QFileDialog.getExistingDirectory(
            self, "选择导出文件夹", "./")
        if self.folder1_path:
            self.textEdit.append(f"切割:输出文件夹为{self.folder1_path}")
    # 点击切割按钮,调用FFmpegApi的extract_video函数,切割视频
    def process_extract(self):
        # 检测程序是否输入了视频文件夹
        if not hasattr(self, 'folder_path1'):
            self.textEdit.append("切割:请先输入视频文件夹")
            return
        if not hasattr(self, 'folder1_path'):
            self.textEdit.append("切割:请先输入视频文件夹")
            return
        # 开始切割视频
        # 读取片头时间和片尾时间,以及编码格式
        start_time = self.time1.text()
        end_time = self.time2.text()
        encoder = self.line.text()
        if self.folder_path1 and self.folder1_path:
            # 实例化FFmpegApi
            ffmpeg_instance = FFmpeg(ffpath.ffmpeg_path)
            # 调用extract_video函数
            ffmpeg_instance.extract_video(self.folder_path1, start_time, end_time,self.folder1_path, encoder)
            self.textEdit.append("视频切割完成")
        else:
            self.textEdit.append("切割:请先输入视频文件夹")

concat_video function

    # 合并流程
    # 点击导入视频文件夹按钮,弹出文件选择对话框,选择视频文件夹,选择完成后显示在文本框中
    def import_video_folder2(self):
        self.folder_path2 = QFileDialog.getExistingDirectory(
            self, "选择视频文件夹", "./")
        if self.folder_path2:
            self.textEdit.append(f"合并:输入文件夹为{self.folder_path2}")
    # 点击导入片头视频文件按钮,弹出文件选择对话框,选择视频文件,选择完成后添加到文本框中且不覆盖前面的文字内容
    def import_video_file1(self):
        self.file1_path, _ = QFileDialog.getOpenFileName(
            self, "选择视频文件", "./", "视频文件 (*.mp4)")
        if self.file1_path:
            self.textEdit.append(f"合并:输入片头文件为{self.file1_path}")

    # 点击导入片尾视频文件按钮,弹出文件选择对话框,选择视频文件,选择完成后添加到文本框中且不覆盖前面的文字内容
    def import_video_file2(self):
        self.file2_path, _ = QFileDialog.getOpenFileName(
            self, "选择视频文件", "./", "视频文件 (*.mp4)")
        if self.file2_path:
            self.textEdit.append(f"合并:输入片尾文件为{self.file2_path}")

    # 点击导出合并文件夹按钮,弹出文件选择对话框,选择文件夹,选择完成后显示在文本框中
    def export_video_folder2(self):
        self.folder2_path = QFileDialog.getExistingDirectory(
            self, "选择导出文件夹", "./")
        if self.folder2_path:
            self.textEdit.append(f"合并:输出文件夹为{self.folder2_path}")
    # 点击开始合并按钮,调用FFmpegApi的merge_video函数,合并视频
    def process_concat(self):
        # 检测程序是否输入了视频文件夹
        if not hasattr(self, 'folder_path2'):
            self.textEdit.append("合并:请先输入视频文件夹")
            return
        if not hasattr(self, 'file1_path'):
            self.textEdit.append("合并:请先输入片头视频文件")
            return
        if not hasattr(self, 'file2_path'):
            self.textEdit.append("合并:请先输入片尾视频文件")
            return
        if not hasattr(self, 'folder2_path'):
            self.textEdit.append("合并:请先输入导出文件夹")
            return
        # 开始合并视频
        # 读取片头时间和片尾时间,以及编码格式
        encoder = self.line2.text()
        if self.folder_path2 and self.file1_path and self.file2_path and self.folder2_path:
            # 实例化FFmpegApi
            ffmpeg_instance = FFmpeg(ffpath.ffmpeg_path)
            # 调用merge_video函数
            ffmpeg_instance.merge_video(self.folder_path2, self.file1_path, self.file2_path, self.folder2_path, encoder)
            self.textEdit.append("视频合并完成")
        else:
            self.textEdit.append("合并:请先输入视频文件夹")

running code

# 运行窗口程序
if __name__ == '__main__':
    app = QApplication([])
    window = MainWindow()  # 创建窗口对象
    window.show()  # 显示窗口
    app.exec_()  # 运行程序

Optimization, Beautification, Packaging & Deployment

Tasks in progress include:

  • Performance Optimization
  • Abort Processing
  • Console Invocation Enablement
  • Redirecting FFmpeg runtime output to console
  • Handling merging of two distinct video segments (currently requires both intro and outro)
  • GUI Enhancement
  • Package Dependencies
  • Encapsulation as BAT or EXE
  • Others

User Guide (as of 20240507)

Download the release package, extract, and run the EXE!

User Guide (as of 20240506)

  1. Set up Python environment and install third-party library pyside6:

    pip install pyside6
    
  2. Place FFmpeg in the main.py directory structured as follows:

    ├── FFmpeg     # Note capitalization
    │   └── bin
    │   └── ...
    ├── main.py     # Main script
    ├── ...
    

    This structure is optional but requires manual path setup within the app.

  3. Run main.py:

    python main.py
    
  4. Alternatively, without installing pyside6 or GUI, directly invoke ffmpegApi.py as detailed in the DETAIL.md debugging section.

Update Log

Created 20240506-1911

Source code uploaded, addressing batch intro and outro processing.

Current Issues

  • Mandates both intro and outro, lacking direct concatenation of two videos.
  • Packaging not yet achieved, pending further exploration.
  • To be determined.

Released 20240507-1904

Packaged version available for elegant execution, albeit noting the substantial size of the PyInstaller output, prompting consideration of alternative packaging techniques.

Built with Hugo
Theme Stack designed by Jimmy
© Licensed Under CC BY-NC-SA 4.0