win10(三)视频剪裁

发布于:2025-09-08 ⋅ 阅读:(18) ⋅ 点赞:(0)

上传一刻相册,有30M大小限制。这个软件能免费剪裁视频而且支持手机的H.265格式,这个格式目前连potplayer都支持不好。但是配合FFmpeg可以检测并且能按大小(或时间)剪裁,并上传到一刻相册上播放。

下载FFmpeg的方法:打开网站

Download FFmpeg

然后选择Windows由gyan.dev构建,再将下载的压缩包解压为文件夹,将文件夹放到自己想放的位置,并在环境变量里添加其中bin文件夹的位置。

例如解压为ffmpeg文件夹放到C盘表面后,在环境变量Path里增加C:\ffmpeg\bin即可。

以下是批量剪裁python代码:

# coding=utf-8
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import subprocess
import os
import threading
import queue
import time
import json

class VideoSplitterApp:
    def __init__(self, root):
        self.root = root
        self.root.title("视频分割工具")
        self.root.geometry("500x400")
        self.root.configure(bg='light yellow')  # 设置背景为黄色
        
        self.video_paths = []
        self.split_method = tk.StringVar()
        self.split_method.set("size")  # 默认按大小分割
        self.split_threads = []
        self.output_dir = None
        self.current_process = None
        self.is_cancelled = False
        self.progress_value = tk.DoubleVar()
        self.current_video_index = 0
        self.progress_queue = queue.Queue()
        
        # 设置字体
        self.font_style = ("FangSong", 12)
        self.button_font_style = ("FangSong", 12, "bold")
        
        # 糖果色按钮颜色
        self.candy_colors = ['#FFB6C1', '#87CEFA', '#98FB98', '#DDA0DD', '#FFD700']
        
        self.create_widgets()
        
        # 定期检查进度队列
        self.check_progress_queue()
        
        # 检查FFmpeg是否可用
        self.check_ffmpeg_available()
        
    def check_ffmpeg_available(self):
        """检查系统是否安装了FFmpeg"""
        try:
            result = subprocess.run(['ffmpeg', '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            if result.returncode != 0:
                messagebox.showerror("错误", "未找到FFmpeg。请确保已正确安装FFmpeg并将其添加到系统PATH中。")
        except FileNotFoundError:
            messagebox.showerror("错误", "未找到FFmpeg。请确保已正确安装FFmpeg并将其添加到系统PATH中。")
        
    def create_widgets(self):
        # 文件选择部分
        self.file_frame = tk.Frame(self.root, bg='light yellow')
        self.file_frame.pack(pady=10, fill=tk.X, padx=10)
        
        self.select_button = tk.Button(self.file_frame, text="选择视频文件", 
                                      command=self.select_video, 
                                      bg=self.candy_colors[0],
                                      font=self.button_font_style,
                                      relief=tk.RAISED, bd=2)
        self.select_button.pack(side=tk.LEFT, padx=5)
        
        self.file_label = tk.Label(self.file_frame, text="未选择文件", 
                                  wraplength=300, bg='light yellow', 
                                  font=self.font_style)
        self.file_label.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
        
        # 分割方式选择
        self.method_frame = tk.Frame(self.root, bg='light yellow')
        self.method_frame.pack(pady=10, fill=tk.X, padx=10)
        
        self.time_radio = tk.Radiobutton(self.method_frame, text="按时间分割", 
                                        variable=self.split_method, value="time", 
                                        command=self.toggle_input,
                                        bg='light yellow', font=self.font_style)
        self.time_radio.pack(side=tk.LEFT, padx=5)
        
        self.size_radio = tk.Radiobutton(self.method_frame, text="按大小分割", 
                                        variable=self.split_method, value="size", 
                                        command=self.toggle_input,
                                        bg='light yellow', font=self.font_style)
        self.size_radio.pack(side=tk.LEFT, padx=5)
        
        # 输入框
        self.input_frame = tk.Frame(self.root, bg='light yellow')
        self.input_frame.pack(pady=10, fill=tk.X, padx=10)
        
        self.time_label = tk.Label(self.input_frame, text="分割时间(秒):",
                                  bg='light yellow', font=self.font_style)
        self.time_label.pack(side=tk.LEFT, padx=5)
        
        self.time_entry = tk.Entry(self.input_frame, width=10, font=self.font_style)
        self.time_entry.pack(side=tk.LEFT, padx=5)
        
        self.size_label = tk.Label(self.input_frame, text="分割大小(MB):",
                                  bg='light yellow', font=self.font_style)
        self.size_label.pack(side=tk.LEFT, padx=5)
        
        self.size_entry = tk.Entry(self.input_frame, width=10, font=self.font_style)
        self.size_entry.insert(0, "28")  # 默认大小20MB
        self.size_entry.pack(side=tk.LEFT, padx=5)
        
        self.toggle_input()  # 初始化时禁用不需要的输入框
        
        # 输出目录选择
        self.output_frame = tk.Frame(self.root, bg='light yellow')
        self.output_frame.pack(pady=5, fill=tk.X, padx=10)
        
        self.output_button = tk.Button(self.output_frame, 
                                     text="选择输出文件夹",
                                     command=self.select_output_dir,
                                     bg=self.candy_colors[1],
                                     font=self.button_font_style,
                                     relief=tk.RAISED, bd=2)
        self.output_button.pack(side=tk.LEFT, padx=5)
        
        self.output_label = tk.Label(self.output_frame, 
                                   text="默认在原目录创建split_output",
                                   wraplength=300, bg='light yellow', 
                                   font=self.font_style)
        self.output_label.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
        
        # 进度条
        self.progress_frame = tk.Frame(self.root, bg='light yellow')
        self.progress_frame.pack(pady=10, fill=tk.X, padx=10)
        
        self.progress_label = tk.Label(self.progress_frame, text="准备就绪",
                                      bg='light yellow', font=self.font_style)
        self.progress_label.pack(fill=tk.X)
        
        self.progress_bar = ttk.Progressbar(self.progress_frame, 
                                           variable=self.progress_value,
                                           maximum=100)
        self.progress_bar.pack(fill=tk.X, pady=5)
        
        # 操作按钮
        self.button_frame = tk.Frame(self.root, bg='light yellow')
        self.button_frame.pack(pady=10, fill=tk.X, padx=10)
        
        self.start_button = tk.Button(self.button_frame, text="开始分割", 
                                     command=self.start_split,
                                     bg=self.candy_colors[2],
                                     font=self.button_font_style,
                                     relief=tk.RAISED, bd=2)
        self.start_button.pack(side=tk.LEFT, padx=5)
        
        self.cancel_button = tk.Button(self.button_frame, text="取消", 
                                      command=self.cancel_split,
                                      bg=self.candy_colors[3],
                                      font=self.button_font_style,
                                      relief=tk.RAISED, bd=2)
        self.cancel_button.pack(side=tk.LEFT, padx=5)
    
    def check_progress_queue(self):
        """定期检查进度队列并更新UI"""
        try:
            while True:
                message = self.progress_queue.get_nowait()
                if message.startswith("PROGRESS:"):
                    progress = float(message.split(":")[1])
                    self.progress_value.set(progress)
                else:
                    self.progress_label.config(text=message)
        except queue.Empty:
            pass
        finally:
            # 每100毫秒检查一次队列
            self.root.after(100, self.check_progress_queue)
    
    def select_video(self):
        file_paths = filedialog.askopenfilenames(
            title="选择视频文件",
            filetypes=[("视频文件", "*.mp4 *.avi *.mkv *.mov *.flv *.wmv *.webm")]
        )
        if file_paths:
            self.video_paths = list(file_paths)
            if len(file_paths) == 1:
                self.file_label.config(text=os.path.basename(file_paths[0]))
            else:
                self.file_label.config(text=f"已选择 {len(file_paths)} 个文件")
    
    def toggle_input(self):
        if self.split_method.get() == "time":
            self.time_entry.config(state="normal")
            self.size_entry.config(state="disabled")
        else:
            self.size_entry.config(state="normal")
            self.time_entry.config(state="disabled")
    
    def validate_input(self):
        if not self.video_paths:
            messagebox.showerror("错误", "请选择视频文件!")
            return False
            
        # 验证每个文件的时长
        for path in self.video_paths:
            if not os.path.exists(path):
                messagebox.showerror("错误", f"文件不存在:{path}")
                return False
                
            if self.split_method.get() == "time":
                try:
                    interval = float(self.time_entry.get())
                    if interval <= 0:
                        raise ValueError
                    duration = self.get_video_duration(path)
                    if duration == 0:
                        return False
                    if interval > duration:
                        messagebox.showwarning("警告", 
                            f"分割时间大于视频时长({duration:.2f}秒),将只生成一个片段")
                except ValueError:
                    messagebox.showerror("错误", "请输入有效的分割时间!")
                    return False
            else:
                try:
                    size = int(self.size_entry.get())
                    if size <= 0:
                        raise ValueError
                except ValueError:
                    messagebox.showerror("错误", "请输入有效的分割大小!")
                    return False
            
        return True
    
    def get_video_duration(self, input_video):
        """获取视频时长,支持H.265编码"""
        try:
            # 使用JSON格式输出,更容易解析
            cmd = [
                'ffprobe', '-v', 'quiet',
                '-print_format', 'json',
                '-show_format',
                '-show_streams',
                input_video
            ]
            
            result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            
            if result.returncode != 0:
                # 如果标准方法失败,尝试使用更兼容的方法
                return self.get_video_duration_fallback(input_video)
            
            # 解析JSON输出
            data = json.loads(result.stdout)
            
            # 首先尝试从format获取时长
            if 'format' in data and 'duration' in data['format']:
                return float(data['format']['duration'])
            
            # 然后尝试从视频流获取时长
            if 'streams' in data:
                for stream in data['streams']:
                    if stream['codec_type'] == 'video' and 'duration' in stream:
                        return float(stream['duration'])
            
            # 如果以上方法都失败,使用备用方法
            return self.get_video_duration_fallback(input_video)
            
        except Exception as e:
            print(f"获取视频时长错误: {e}")
            return self.get_video_duration_fallback(input_video)
    
    def get_video_duration_fallback(self, input_video):
        """备用方法获取视频时长"""
        try:
            # 尝试使用不同的参数获取视频时长
            cmd = [
                'ffprobe', '-v', 'error',
                '-show_entries', 'format=duration',
                '-of', 'default=noprint_wrappers=1:nokey=1',
                input_video
            ]
            
            result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            
            if result.returncode == 0:
                return float(result.stdout)
            
            # 如果仍然失败,尝试使用更兼容的方法
            cmd = [
                'ffprobe', '-i', input_video,
                '-show_entries', 'format=duration',
                '-v', 'quiet',
                '-of', 'csv=p=0'
            ]
            
            result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            
            if result.returncode == 0:
                return float(result.stdout)
            
            # 如果所有方法都失败,显示错误信息
            error_msg = f"无法获取视频时长: {input_video}\n"
            error_msg += "可能的原因:\n"
            error_msg += "1. 视频文件损坏\n"
            error_msg += "2. 不支持的视频编码格式(如H.265)\n"
            error_msg += "3. FFmpeg版本过旧\n"
            error_msg += "请尝试更新FFmpeg到最新版本"
            
            self.progress_queue.put(error_msg)
            return 0
            
        except Exception as e:
            error_msg = f"获取视频时长错误: {str(e)}"
            self.progress_queue.put(error_msg)
            return 0
    
    def split_video(self, input_video, interval_sec=0, size_mb=0):
        if self.is_cancelled:
            return
            
        # 更新进度标签
        self.progress_queue.put(f"处理中: {os.path.basename(input_video)}")
        
        # 确定输出目录
        if self.output_dir:
            output_dir = self.output_dir
        else:
            original_dir = os.path.dirname(input_video)
            output_dir = os.path.join(original_dir, "split_output")
        os.makedirs(output_dir, exist_ok=True)
        
        # 生成输出路径
        base_name = os.path.basename(input_video)
        base, ext = os.path.splitext(base_name)
        output_template = os.path.join(output_dir, f"{base}_%03d{ext}")
        
        # 获取视频时长
        duration = self.get_video_duration(input_video)
        if duration == 0:
            self.progress_queue.put(f"错误: 无法获取 {os.path.basename(input_video)} 的时长")
            return
        
        if interval_sec:
            # 按时间分割
            cmd = [
                'ffmpeg', '-i', input_video, '-c', 'copy',
                '-f', 'segment', '-segment_time', str(interval_sec),
                '-reset_timestamps', '1', '-y', output_template
            ]
        else:
            # 按大小分割
            file_size = os.path.getsize(input_video)
            # 计算目标比特率 (bps)
            target_size_bits = size_mb * 1024 * 1024 * 8
            # 计算分割时间
            segment_time = (target_size_bits * duration) / (file_size * 8)
            
            cmd = [
                'ffmpeg', '-i', input_video, '-c', 'copy',
                '-f', 'segment', '-segment_time', str(segment_time),
                '-reset_timestamps', '1', '-y', output_template
            ]
        
        try:
            # 启动进程
            process = subprocess.Popen(
                cmd, 
                stdout=subprocess.PIPE, 
                stderr=subprocess.STDOUT,
                universal_newlines=True,
                bufsize=1
            )
            
            # 读取输出并更新进度
            for line in process.stdout:
                if self.is_cancelled:
                    process.terminate()
                    break
                    
                # 解析ffmpeg输出以获取进度
                if "time=" in line:
                    time_pos = line.find("time=")
                    if time_pos != -1:
                        time_str = line[time_pos+5:].split()[0]
                        try:
                            # 将时间转换为秒
                            h, m, s = time_str.split(':')
                            current_time = int(h)*3600 + int(m)*60 + float(s)
                            
                            # 计算进度百分比
                            if duration > 0:
                                progress = (current_time / duration) * 100
                                self.progress_queue.put(f"PROGRESS:{progress}")
                        except:
                            pass
            
            # 等待进程完成
            process.wait()
            
            if process.returncode == 0 and not self.is_cancelled:
                self.progress_queue.put(f"完成: {os.path.basename(input_video)}")
            elif self.is_cancelled:
                self.progress_queue.put("已取消")
            else:
                self.progress_queue.put(f"错误: {os.path.basename(input_video)}")
                
        except Exception as e:
            self.progress_queue.put(f"错误: {str(e)}")
    
    def start_split(self):
        if not self.validate_input():
            return
        
        self.is_cancelled = False
        self.current_video_index = 0
        self.progress_value.set(0)
        self.progress_queue.put("开始处理...")
        
        # 禁用开始按钮,防止重复点击
        self.start_button.config(state=tk.DISABLED)
        
        # 启动一个线程来处理所有视频
        worker_thread = threading.Thread(target=self.process_all_videos)
        worker_thread.daemon = True  # 设置为守护线程,主程序退出时自动结束
        worker_thread.start()
    
    def process_all_videos(self):
        """处理所有视频的线程函数"""
        for i, video_path in enumerate(self.video_paths):
            if self.is_cancelled:
                break
                
            self.current_video_index = i
            
            if self.split_method.get() == "time":
                interval = float(self.time_entry.get())
                self.split_video(video_path, interval)
            else:
                size = int(self.size_entry.get())
                self.split_video(video_path, 0, size)
        
        if not self.is_cancelled:
            self.progress_queue.put("所有任务完成!")
            # 在主线程中显示消息框
            self.root.after(0, lambda: messagebox.showinfo("完成", "所有视频分割完成!"))
        
        # 重新启用开始按钮
        self.root.after(0, lambda: self.start_button.config(state=tk.NORMAL))
    
    def select_output_dir(self):
        """选择输出目录"""
        dir_path = filedialog.askdirectory(title="选择输出文件夹")
        if dir_path:
            self.output_dir = dir_path
            self.output_label.config(text=f"输出目录: {dir_path}")
    
    def cancel_split(self):
        self.is_cancelled = True
        if self.current_process:
            self.current_process.terminate()
        self.progress_queue.put("取消中...")
        
        # 重新启用开始按钮
        self.start_button.config(state=tk.NORMAL)
        
def main():
    root = tk.Tk()
    app = VideoSplitterApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()
 


网站公告

今日签到

点亮在社区的每一天
去签到