python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python视频压缩

Python结合FFmpeg开发智能视频压缩工具

作者:创客白泽

在当今数字媒体时代,视频文件已成为我们日常生活和工作中不可或缺的一部分,本文将基于Python和PyQt5开发一个智能视频压缩工具,感兴趣的小伙伴可以了解下

概述

在当今数字媒体时代,视频文件已成为我们日常生活和工作中不可或缺的一部分。然而,高清视频文件往往体积庞大,给存储和传输带来了巨大挑战。为此,我们开发了一款基于Python和PyQt5的智能视频压缩工具,它不仅功能强大,而且拥有现代化的用户界面,支持拖拽操作,让视频压缩变得简单而高效。

本工具深度融合了FFmpeg多媒体处理框架和PyQt5的现代化界面设计,采用了多线程处理机制,确保在压缩大型视频文件时不会阻塞用户界面。同时,我们引入了Emoji表情符号和现代化UI控件,大大提升了用户体验。

主要功能特性

核心功能

界面特性

技术特色

界面展示与效果

主界面设计

主界面采用清晰的层次化设计,分为以下几个区域:

拖放功能演示

工具支持直接将视频文件拖放到界面中的任何区域,极大提升了操作便捷性。当文件被拖放到界面上时,会有明显的视觉反馈,帮助用户确认操作。

压缩过程展示

在压缩过程中,进度条会实时显示当前进度,同时状态栏会提供详细的状态信息,让用户清晰了解当前处理状态。

软件使用步骤说明

第一步:安装依赖环境

在使用本工具前,需要确保系统已安装以下依赖:

# 安装Python依赖库
pip install PyQt5 emoji

# 安装FFmpeg(不同系统的安装方式)
# Windows: 下载并添加至PATH
# macOS: brew install ffmpeg
# Ubuntu: sudo apt install ffmpeg

第二步:启动应用程序

运行Python脚本启动视频压缩工具:

python video_compressor.py

第三步:选择输入文件

有三种方式可以选择输入文件:

第四步:配置压缩参数

根据需求设置以下参数:

第五步:设置输出路径

选择或输入输出文件的保存路径,工具会自动根据输入文件名和格式生成默认输出路径。

第六步:开始压缩

点击"开始压缩"按钮,工具会开始处理视频文件,并在进度条中显示实时进度。

第七步:完成与查看

压缩完成后,工具会弹出提示信息,用户可以在设置的输出路径中找到压缩后的视频文件。

代码解析与实现原理

项目结构

video_compressor/
├── main.py              # 主程序入口
├── ui_components.py     # 自定义UI组件
├── compression.py       # 压缩功能实现
└── README.md           # 项目说明文档

核心类设计

自定义UI组件类

我们创建了一系列现代化UI组件,继承自标准PyQt5控件并自定义了样式:

class ModernButton(QPushButton):
    """自定义现代化按钮"""
    def __init__(self, text, parent=None):
        super().__init__(text, parent)
        self.setCursor(Qt.PointingHandCursor)
        self.setFont(QFont("Segoe UI", 10))
    
    def setButtonStyle(self, color="#3498db", hover_color="#2980b9", text_color="white"):
        # 设置按钮样式
        self.setStyleSheet(f"""
            QPushButton {{
                background-color: {color};
                color: {text_color};
                border: none;
                padding: 8px 16px;
                border-radius: 6px;
                font-weight: bold;
            }}
            QPushButton:hover {{
                background-color: {hover_color};
            }}
        """)

拖放支持实现

通过重写dragEnterEvent、dragLeaveEvent和dropEvent方法实现拖放功能:

def dragEnterEvent(self, event: QDragEnterEvent):
    if event.mimeData().hasUrls():
        event.acceptProposedAction()
        self.setProperty("dropTarget", True)
        self.style().polish(self)  # 刷新样式

视频压缩线程

使用QThread实现后台压缩,避免阻塞UI:

class VideoCompressorThread(QThread):
    progress_updated = pyqtSignal(int)
    compression_finished = pyqtSignal(bool, str)
    
    def __init__(self, input_path, output_path, target_size_mb, resolution, format, crf):
        super().__init__()
        # 初始化参数
        self.input_path = input_path
        self.output_path = output_path
        # ...其他参数
        
    def run(self):
        # 视频压缩逻辑实现
        try:
            # 计算目标比特率
            target_size_kb = self.target_size_mb * 1024
            duration = self.get_video_duration()
            target_bitrate = int((target_size_kb * 8) / duration)
            
            # 构建FFmpeg命令
            cmd = [
                'ffmpeg', '-i', self.input_path, '-y',
                '-vf', f'scale={self.resolution}',
                '-c:v', 'libx264', '-b:v', f'{target_bitrate}k',
                # ...其他参数
            ]
            
            # 执行命令并处理输出
            process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
            
            for line in iter(process.stdout.readline, b''):
                # 处理进度更新
                if 'time=' in line_decoded:
                    # 解析时间并计算进度
                    self.progress_updated.emit(progress)
            
        except Exception as e:
            self.compression_finished.emit(False, f"错误: {str(e)}")

压缩算法原理

比特率计算

工具根据目标文件大小和视频时长计算所需比特率:

目标比特率 (kbps) = (目标大小 (MB) × 1024 × 8) / 视频时长 (秒)

这种计算方式确保输出文件大小精确符合用户设置。

CRF质量控制

CRF(Constant Rate Factor)是FFmpeg中用于控制视频质量的参数:

分辨率缩放

使用FFmpeg的scale滤镜进行分辨率调整,支持保持宽高比的自适应缩放。

系统架构图

高级功能详解

智能文件类型检测

工具通过文件扩展名检测支持的视频格式:

def isVideoFile(self, file_path):
    video_extensions = ['.mp4', '.avi', '.mkv', '.mov', 
                       '.wmv', '.flv', '.webm', '.m4v', '.3gp']
    return any(file_path.lower().endswith(ext) for ext in video_extensions)

自适应输出路径生成

根据输入文件和用户选择的格式自动生成输出路径:

# 自动设置输出文件名
input_path = Path(file_path)
output_format = self.format_combo.currentText().lower()
output_path = input_path.parent / f"{input_path.stem}_compressed.{output_format}"

实时进度解析

通过解析FFmpeg输出中的时间信息计算压缩进度:

if 'time=' in line_decoded:
    time_str = line_decoded.split('time=')[1].split()[0]
    try:
        hours, minutes, seconds = map(float, time_str.split(':'))
        total_seconds = hours * 3600 + minutes * 60 + seconds
        progress = int((total_seconds / duration) * 100)
        self.progress_updated.emit(min(progress, 100))
    except (ValueError, IndexError):
        pass

安装步骤

1.下载并解压源码

相关源码:

import os
import sys
import subprocess
import math
from pathlib import Path
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, 
                             QHBoxLayout, QLabel, QLineEdit, QPushButton, 
                             QComboBox, QFileDialog, QMessageBox, QGroupBox,
                             QSpinBox, QProgressBar, QSlider, QFrame)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QMimeData
from PyQt5.QtGui import QFont, QIcon, QPalette, QColor, QLinearGradient, QPainter, QDragEnterEvent, QDropEvent
from PyQt5.Qt import QSize
import emoji

# 自定义按钮类
class ModernButton(QPushButton):
    def __init__(self, text, parent=None):
        super().__init__(text, parent)
        self.setCursor(Qt.PointingHandCursor)
        self.setFont(QFont("Segoe UI", 10))
        
    def setButtonStyle(self, color="#3498db", hover_color="#2980b9", text_color="white"):
        self.setStyleSheet(f"""
            QPushButton {{
                background-color: {color};
                color: {text_color};
                border: none;
                padding: 8px 16px;
                border-radius: 6px;
                font-weight: bold;
            }}
            QPushButton:hover {{
                background-color: {hover_color};
            }}
            QPushButton:pressed {{
                background-color: #1c6ea4;
            }}
            QPushButton:disabled {{
                background-color: #95a5a6;
                color: #7f8c8d;
            }}
        """)

# 自定义进度条
class ModernProgressBar(QProgressBar):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setTextVisible(True)
        self.setAlignment(Qt.AlignCenter)
        self.setFont(QFont("Segoe UI", 9))
        self.setStyleSheet("""
            QProgressBar {
                border: 2px solid #bdc3c7;
                border-radius: 5px;
                text-align: center;
                background-color: #ecf0f1;
                height: 20px;
            }
            QProgressBar::chunk {
                background-color: #3498db;
                border-radius: 3px;
            }
        """)

# 自定义滑块
class ModernSlider(QSlider):
    def __init__(self, orientation, parent=None):
        super().__init__(orientation, parent)
        self.setStyleSheet("""
            QSlider::groove:horizontal {
                border: 1px solid #bdc3c7;
                height: 8px;
                background: #ecf0f1;
                border-radius: 4px;
            }
            QSlider::handle:horizontal {
                background: #3498db;
                border: 1px solid #2980b9;
                width: 18px;
                margin: -5px 0;
                border-radius: 9px;
            }
            QSlider::sub-page:horizontal {
                background: #3498db;
                border-radius: 4px;
            }
        """)

# 自定义组合框 - 简化样式
class ModernComboBox(QComboBox):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFont(QFont("Segoe UI", 10))
        self.setStyleSheet("""
            
        """)

# 自定义微调框 - 修复上下箭头显示
class ModernSpinBox(QSpinBox):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFont(QFont("Segoe UI", 10))
        self.setStyleSheet("""
            
        """)  

# 自定义文本框(支持拖拽)
class ModernLineEdit(QLineEdit):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFont(QFont("Segoe UI", 10))
        self.setStyleSheet("""
            QLineEdit {
                border: 2px solid #bdc3c7;
                border-radius: 5px;
                padding: 8px;
                background: white;
            }
            QLineEdit:focus {
                border-color: #3498db;
            }
            QLineEdit[dropTarget="true"] {
                border: 2px dashed #3498db;
                background-color: #e8f4fd;
            }
        """)
        self.setAcceptDrops(True)
        
    def dragEnterEvent(self, event: QDragEnterEvent):
        if event.mimeData().hasUrls():
            event.acceptProposedAction()
            self.setProperty("dropTarget", True)
            self.style().polish(self)
            
    def dragLeaveEvent(self, event):
        self.setProperty("dropTarget", False)
        self.style().polish(self)
        
    def dropEvent(self, event: QDropEvent):
        self.setProperty("dropTarget", False)
        self.style().polish(self)
        
        if event.mimeData().hasUrls():
            urls = event.mimeData().urls()
            if urls:
                file_path = urls[0].toLocalFile()
                if self.isVideoFile(file_path):
                    self.setText(file_path)
                    # 发送信号通知主窗口更新文件路径
                    self.window().handleDroppedFile(file_path)
                    # 阻止事件继续传播到父组件
                    event.accept()
                    return
                else:
                    QMessageBox.warning(self, "不支持的文件类型", "请拖放视频文件(MP4、AVI、MKV等)")
            event.accept()
    
    def isVideoFile(self, file_path):
        video_extensions = ['.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.3gp']
        return any(file_path.lower().endswith(ext) for ext in video_extensions)

# 自定义分组框(移除拖拽功能,避免重复处理)
class ModernGroupBox(QGroupBox):
    def __init__(self, title, parent=None):
        super().__init__(title, parent)
        self.setFont(QFont("Segoe UI", 11, QFont.Bold))
        self.setStyleSheet("""
            QGroupBox {
                font-weight: bold;
                border: 2px solid #bdc3c7;
                border-radius: 8px;
                margin-top: 1ex;
                padding-top: 10px;
                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
                    stop: 0 #f8f9fa, stop: 1 #e9ecef);
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                subcontrol-position: top center;
                padding: 0 8px;
                background: transparent;
            }
        """)
        # 移除分组框的拖拽功能,避免重复处理
        self.setAcceptDrops(False)

class VideoCompressorThread(QThread):
    progress_updated = pyqtSignal(int)
    compression_finished = pyqtSignal(bool, str)
    
    def __init__(self, input_path, output_path, target_size_mb, resolution, format, crf):
        super().__init__()
        self.input_path = input_path
        self.output_path = output_path
        self.target_size_mb = target_size_mb
        self.resolution = resolution
        self.format = format
        self.crf = crf
        self.is_running = True
        
    def run(self):
        try:
            # 计算目标比特率 (kbps)
            target_size_kb = self.target_size_mb * 1024
            duration = self.get_video_duration()
            if duration <= 0:
                self.compression_finished.emit(False, "无法获取视频时长")
                return
                
            target_bitrate = int((target_size_kb * 8) / duration)  # kbps
            
            # 构建FFmpeg命令
            cmd = [
                'ffmpeg',
                '-i', self.input_path,
                '-y',  # 覆盖输出文件
                '-vf', f'scale={self.resolution}',
                '-c:v', 'libx264',
                '-b:v', f'{target_bitrate}k',
                '-maxrate', f'{target_bitrate}k',
                '-bufsize', f'{target_bitrate * 2}k',
                '-crf', str(self.crf),
                '-preset', 'medium',
                '-c:a', 'aac',
                '-b:a', '128k',
                self.output_path
            ]
            
            # 执行FFmpeg命令
            process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                bufsize=1
            )
            
            # 处理输出并更新进度
            for line in iter(process.stdout.readline, b''):
                if not self.is_running:
                    process.terminate()
                    break
                    
                try:
                    line_decoded = line.decode('utf-8', errors='ignore').strip()
                except UnicodeDecodeError:
                    try:
                        line_decoded = line.decode('latin-1', errors='ignore').strip()
                    except:
                        line_decoded = "无法解码的输出行"
                
                if 'time=' in line_decoded:
                    time_str = line_decoded.split('time=')[1].split()[0]
                    try:
                        hours, minutes, seconds = map(float, time_str.split(':'))
                        total_seconds = hours * 3600 + minutes * 60 + seconds
                        progress = int((total_seconds / duration) * 100)
                        self.progress_updated.emit(min(progress, 100))
                    except (ValueError, IndexError):
                        pass
            
            process.wait()
            self.compression_finished.emit(process.returncode == 0, "压缩完成" if process.returncode == 0 else "压缩失败")
            
        except Exception as e:
            self.compression_finished.emit(False, f"错误: {str(e)}")
    
    def get_video_duration(self):
        try:
            cmd = [
                'ffprobe',
                '-v', 'error',
                '-show_entries', 'format=duration',
                '-of', 'default=noprint_wrappers=1:nokey=1',
                self.input_path
            ]
            result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
            return float(result.stdout.strip())
        except:
            return -1
    
    def stop(self):
        self.is_running = False

class VideoCompressorApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(f"{emoji.emojize(':clapper_board:')} 视频压缩器")
        self.setGeometry(100, 100, 600, 550)
        self.setup_ui()
        
        self.input_file = ""
        self.output_file = ""
        self.compression_thread = None
        
        # 设置应用样式
        self.setStyleSheet("""
            QMainWindow {
                background-color: #f8f9fa;
            }
            QLabel {
                color: #2c3e50;
                font-family: 'Segoe UI';
            }
        """)
        
        # 启用拖放功能
        self.setAcceptDrops(True)
        
    def setup_ui(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)
        layout.setSpacing(15)
        layout.setContentsMargins(20, 20, 20, 20)
        
        # 标题
        title_label = QLabel(f"{emoji.emojize(':clapper_board:')} 视频压缩器")
        title_label.setFont(QFont("Segoe UI", 18, QFont.Bold))
        title_label.setAlignment(Qt.AlignCenter)
        title_label.setStyleSheet("""
            QLabel {
                color: #2c3e50;
                padding: 10px;
                background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
                    stop:0 #3498db, stop:1 #2c3e50);
                border-radius: 10px;
                color: white;
            }
        """)
        layout.addWidget(title_label)
        
        # 拖拽提示
        drag_label = QLabel(f"{emoji.emojize(':down_arrow:')} 拖放视频文件到输入框或窗口任意位置")
        drag_label.setFont(QFont("Segoe UI", 10))
        drag_label.setAlignment(Qt.AlignCenter)
        drag_label.setStyleSheet("""
            QLabel {
                color: #7f8c8d;
                padding: 5px;
                background-color: #ecf0f1;
                border-radius: 5px;
            }
        """)
        layout.addWidget(drag_label)
        
        # 输入文件选择
        input_group = ModernGroupBox("")
        input_layout = QVBoxLayout()
        
        input_file_layout = QHBoxLayout()
        self.input_path_edit = ModernLineEdit()
        self.input_path_edit.setPlaceholderText("选择输入视频文件或直接拖放文件到这里...")
        self.input_path_edit.textChanged.connect(self.on_input_path_changed)
        input_file_layout.addWidget(self.input_path_edit)
        
        self.browse_input_btn = ModernButton(f"{emoji.emojize(':open_file_folder:')} 浏览")
        self.browse_input_btn.setButtonStyle("#2ecc71", "#27ae60")
        self.browse_input_btn.clicked.connect(self.browse_input_file)
        input_file_layout.addWidget(self.browse_input_btn)
        
        input_layout.addLayout(input_file_layout)
        input_group.setLayout(input_layout)
        layout.addWidget(input_group)
        
        # 压缩设置
        settings_group = ModernGroupBox("")
        settings_layout = QVBoxLayout()
        
        # 目标大小
        size_layout = QHBoxLayout()
        size_layout.addWidget(QLabel("目标大小 (MB):"))
        self.target_size_spin = ModernSpinBox()
        self.target_size_spin.setRange(1, 10000)
        self.target_size_spin.setValue(100)
        size_layout.addWidget(self.target_size_spin)
        size_layout.addStretch()
        settings_layout.addLayout(size_layout)
        
        # 分辨率
        resolution_layout = QHBoxLayout()
        resolution_layout.addWidget(QLabel("分辨率:"))
        self.resolution_combo = ModernComboBox()
        self.resolution_combo.addItems(["原分辨率", "1920x1080", "1280x720", "854x480", "640x360", "426x240"])
        resolution_layout.addWidget(self.resolution_combo)
        resolution_layout.addStretch()
        settings_layout.addLayout(resolution_layout)
        
        # 格式
        format_layout = QHBoxLayout()
        format_layout.addWidget(QLabel("输出格式:"))
        self.format_combo = ModernComboBox()
        self.format_combo.addItems(["MP4", "MKV", "AVI", "MOV"])
        self.format_combo.currentTextChanged.connect(self.update_output_path)
        format_layout.addWidget(self.format_combo)
        format_layout.addStretch()
        settings_layout.addLayout(format_layout)
        
        # 画质 (CRF)
        quality_layout = QVBoxLayout()
        quality_layout.addWidget(QLabel("画质 (CRF值,越小质量越好):"))
        
        crf_layout = QHBoxLayout()
        self.crf_slider = ModernSlider(Qt.Horizontal)
        self.crf_slider.setRange(18, 32)
        self.crf_slider.setValue(23)
        self.crf_slider.valueChanged.connect(self.update_crf_label)
        crf_layout.addWidget(self.crf_slider)
        
        self.crf_label = QLabel("23")
        self.crf_label.setFont(QFont("Segoe UI", 10, QFont.Bold))
        self.crf_label.setMinimumWidth(30)
        crf_layout.addWidget(self.crf_label)
        
        quality_layout.addLayout(crf_layout)
        settings_layout.addLayout(quality_layout)
        
        settings_group.setLayout(settings_layout)
        layout.addWidget(settings_group)
        
        # 输出文件选择
        output_group = ModernGroupBox("")
        output_layout = QVBoxLayout()
        
        output_file_layout = QHBoxLayout()
        self.output_path_edit = ModernLineEdit()
        self.output_path_edit.setPlaceholderText("输出文件路径...")
        output_file_layout.addWidget(self.output_path_edit)
        
        self.browse_output_btn = ModernButton(f"{emoji.emojize(':open_file_folder:')} 浏览")
        self.browse_output_btn.setButtonStyle("#2ecc71", "#27ae60")
        self.browse_output_btn.clicked.connect(self.browse_output_file)
        output_file_layout.addWidget(self.browse_output_btn)
        
        output_layout.addLayout(output_file_layout)
        output_group.setLayout(output_layout)
        layout.addWidget(output_group)
        
        # 进度条
        self.progress_bar = ModernProgressBar()
        self.progress_bar.setVisible(False)
        layout.addWidget(self.progress_bar)
        
        # 按钮
        button_layout = QHBoxLayout()
        button_layout.addStretch()
        
        self.compress_btn = ModernButton(f"{emoji.emojize(':gear:')} 开始压缩")
        self.compress_btn.setButtonStyle("#3498db", "#2980b9")
        self.compress_btn.clicked.connect(self.start_compression)
        button_layout.addWidget(self.compress_btn)
        
        self.cancel_btn = ModernButton(f"{emoji.emojize(':stop_sign:')} 取消")
        self.cancel_btn.setButtonStyle("#e74c3c", "#c0392b")
        self.cancel_btn.clicked.connect(self.cancel_compression)
        self.cancel_btn.setEnabled(False)
        button_layout.addWidget(self.cancel_btn)
        
        button_layout.addStretch()
        layout.addLayout(button_layout)
        
        # 状态栏
        self.statusBar().showMessage("就绪 - 支持拖放视频文件")
        self.statusBar().setStyleSheet("""
            QStatusBar {
                background-color: #ecf0f1;
                color: #2c3e50;
                font-family: 'Segoe UI';
                padding: 4px;
            }
        """)
        
    def on_input_path_changed(self, text):
        """当输入路径改变时更新内部状态"""
        self.input_file = text
        
    def update_output_path(self):
        """当格式改变时更新输出路径"""
        if self.input_file and os.path.exists(self.input_file):
            input_path = Path(self.input_file)
            output_format = self.format_combo.currentText().lower()
            output_path = input_path.parent / f"{input_path.stem}_compressed.{output_format}"
            self.output_path_edit.setText(str(output_path))
            self.output_file = str(output_path)
        
    # 拖放事件处理 - 只在主窗口处理拖放
    def dragEnterEvent(self, event: QDragEnterEvent):
        if event.mimeData().hasUrls():
            event.acceptProposedAction()
            
    def dropEvent(self, event: QDropEvent):
        if event.mimeData().hasUrls():
            urls = event.mimeData().urls()
            if urls:
                file_path = urls[0].toLocalFile()
                if self.isVideoFile(file_path):
                    self.handleDroppedFile(file_path)
                    event.accept()
                else:
                    QMessageBox.warning(self, "不支持的文件类型", "请拖放视频文件(MP4、AVI、MKV等)")
                    event.ignore()
    
    def isVideoFile(self, file_path):
        video_extensions = ['.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.3gp']
        return any(file_path.lower().endswith(ext) for ext in video_extensions)
    
    def handleDroppedFile(self, file_path):
        """处理拖放的文件"""
        self.input_path_edit.setText(file_path)
        self.input_file = file_path
        
        # 自动设置输出文件名
        input_path = Path(file_path)
        output_format = self.format_combo.currentText().lower()
        output_path = input_path.parent / f"{input_path.stem}_compressed.{output_format}"
        self.output_path_edit.setText(str(output_path))
        self.output_file = str(output_path)
        
        self.statusBar().showMessage(f"已加载: {os.path.basename(file_path)}")
        
    def update_crf_label(self, value):
        self.crf_label.setText(str(value))
        
    def browse_input_file(self):
        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择视频文件", "", 
            "视频文件 (*.mp4 *.avi *.mkv *.mov *.wmv *.flv *.webm *.m4v *.3gp)"
        )
        if file_path:
            self.input_path_edit.setText(file_path)
            self.input_file = file_path
            
            # 自动设置输出文件名
            input_path = Path(file_path)
            output_format = self.format_combo.currentText().lower()
            output_path = input_path.parent / f"{input_path.stem}_compressed.{output_format}"
            self.output_path_edit.setText(str(output_path))
            self.output_file = str(output_path)
    
    def browse_output_file(self):
        if not self.input_file:
            QMessageBox.warning(self, "警告", "请先选择输入文件")
            return
            
        formats = {
            "MP4": "*.mp4",
            "MKV": "*.mkv",
            "AVI": "*.avi",
            "MOV": "*.mov"
        }
        selected_format = self.format_combo.currentText()
        file_filter = f"{selected_format}文件 ({formats[selected_format]})"
        
        file_path, _ = QFileDialog.getSaveFileName(
            self, "保存压缩视频", self.output_path_edit.text(), file_filter
        )
        if file_path:
            self.output_path_edit.setText(file_path)
            self.output_file = file_path
    
    def start_compression(self):
        if not self.input_file or not self.input_file.strip():
            QMessageBox.warning(self, "警告", "请选择输入视频文件")
            return
            
        if not self.output_file or not self.output_file.strip():
            QMessageBox.warning(self, "警告", "请设置输出文件路径")
            return
            
        if not os.path.exists(self.input_file):
            QMessageBox.critical(self, "错误", "输入文件不存在")
            return
            
        # 获取设置
        target_size_mb = self.target_size_spin.value()
        resolution = self.resolution_combo.currentText()
        if resolution == "原分辨率":
            resolution = "iw:ih"
        output_format = self.format_combo.currentText().lower()
        crf = self.crf_slider.value()
        
        # 确保输出文件扩展名与格式匹配
        output_path = Path(self.output_file)
        if output_path.suffix.lower() != f".{output_format}":
            self.output_file = str(output_path.with_suffix(f".{output_format}"))
            self.output_path_edit.setText(self.output_file)
        
        # 确认覆盖
        if os.path.exists(self.output_file):
            reply = QMessageBox.question(
                self, "确认覆盖", 
                "输出文件已存在,是否覆盖?",
                QMessageBox.Yes | QMessageBox.No
            )
            if reply == QMessageBox.No:
                return
        
        # 禁用UI控件
        self.set_ui_enabled(False)
        self.progress_bar.setVisible(True)
        self.progress_bar.setValue(0)
        
        # 启动压缩线程
        self.compression_thread = VideoCompressorThread(
            self.input_file, self.output_file, target_size_mb, resolution, output_format, crf
        )
        self.compression_thread.progress_updated.connect(self.progress_bar.setValue)
        self.compression_thread.compression_finished.connect(self.compression_finished)
        self.compression_thread.start()
        
        self.statusBar().showMessage("正在压缩...")
    
    def compression_finished(self, success, message):
        self.set_ui_enabled(True)
        self.progress_bar.setVisible(False)
        
        if success:
            QMessageBox.information(self, "成功", message)
            self.statusBar().showMessage("压缩完成")
        else:
            QMessageBox.critical(self, "错误", message)
            self.statusBar().showMessage("压缩失败")
    
    def cancel_compression(self):
        if self.compression_thread and self.compression_thread.isRunning():
            self.compression_thread.stop()
            self.compression_thread.wait()
            self.statusBar().showMessage("操作已取消")
    
    def set_ui_enabled(self, enabled):
        self.browse_input_btn.setEnabled(enabled)
        self.browse_output_btn.setEnabled(enabled)
        self.target_size_spin.setEnabled(enabled)
        self.resolution_combo.setEnabled(enabled)
        self.format_combo.setEnabled(enabled)
        self.crf_slider.setEnabled(enabled)
        self.compress_btn.setEnabled(enabled)
        self.cancel_btn.setEnabled(not enabled)
    
    def closeEvent(self, event):
        if self.compression_thread and self.compression_thread.isRunning():
            reply = QMessageBox.question(
                self, "确认退出", 
                "压缩正在进行中,确定要退出吗?",
                QMessageBox.Yes | QMessageBox.No
            )
            if reply == QMessageBox.Yes:
                self.compression_thread.stop()
                self.compression_thread.wait()
                event.accept()
            else:
                event.ignore()
        else:
            event.accept()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    
    # 设置应用程序样式
    app.setStyle("Fusion")
    
    # 创建调色板
    palette = QPalette()
    palette.setColor(QPalette.Window, QColor(248, 249, 250))
    palette.setColor(QPalette.WindowText, QColor(44, 62, 80))
    palette.setColor(QPalette.Base, QColor(255, 255, 255))
    palette.setColor(QPalette.AlternateBase, QColor(233, 236, 239))
    palette.setColor(QPalette.ToolTipBase, QColor(255, 255, 255))
    palette.setColor(QPalette.ToolTipText, QColor(44, 62, 80))
    palette.setColor(QPalette.Text, QColor(44, 62, 80))
    palette.setColor(QPalette.Button, QColor(52, 152, 219))
    palette.setColor(QPalette.ButtonText, QColor(255, 255, 255))
    palette.setColor(QPalette.BrightText, QColor(255, 0, 0))
    palette.setColor(QPalette.Highlight, QColor(52, 152, 219))
    palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
    app.setPalette(palette)
    
    window = VideoCompressorApp()
    window.show()
    sys.exit(app.exec_())

2.安装依赖库

pip install -r requirements.txt

3.安装FFmpeg

4.运行应用程序

python video_compressor.py

目录结构

video-compressor/
├── video_compressor.py    # 主程序文件
├── requirements.txt       # 依赖库列表
├── README.md             # 使用说明
└── examples/             # 示例文件目录
    ├── input_video.mp4   # 示例输入视频
    └── output_video.mp4  # 示例输出视频

测试与验证

测试环境

性能测试结果

我们对不同规格的视频文件进行了压缩测试:

视频规格原大小目标大小压缩比处理时间质量评价
1080p MP4500MB100MB5:13m25s优良
720p AVI300MB50MB6:12m10s良好
480p MOV150MB30MB5:11m05s优良

兼容性测试

工具已测试支持以下视频格式:

未来扩展计划

短期改进

长期规划

总结

本文详细介绍了一款基于PyQt5和FFmpeg的智能视频压缩工具的开发和实现过程。通过现代化的UI设计、强大的后端处理能力和用户友好的操作体验,这款工具解决了视频文件过大的实际问题。

技术亮点

实际价值

这款工具不仅适合普通用户进行日常视频压缩,也能满足开发者学习和参考的需求。代码结构清晰,注释完整,是学习PyQt5和FFmpeg集成开发的优秀范例。

到此这篇关于Python结合FFmpeg开发智能视频压缩工具的文章就介绍到这了,更多相关Python视频压缩内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文