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