|
| 1 | +import tkinter as tk |
| 2 | +import ttkbootstrap as ttk |
| 3 | +from tkinter import filedialog, messagebox |
| 4 | +import subprocess |
| 5 | +import os |
| 6 | +import cv2 |
| 7 | +from PIL import Image, ImageTk |
| 8 | + |
| 9 | + |
| 10 | +class VideoTrimmerApp(ttk.Window): |
| 11 | + def __init__(self): |
| 12 | + super().__init__(themename='darkly') |
| 13 | + self.title("Video Trimmer") |
| 14 | + self.geometry("1000x400") |
| 15 | + |
| 16 | + # Variables for slider values |
| 17 | + self.start_time = tk.DoubleVar(value=0) |
| 18 | + self.end_time = tk.DoubleVar() |
| 19 | + |
| 20 | + # Button to start encoding |
| 21 | + self.encode_button = tk.Button( |
| 22 | + self, text="Start Encoding", command=self.trim_video |
| 23 | + ) |
| 24 | + self.encode_button.pack(side=tk.RIGHT, anchor='n') |
| 25 | + |
| 26 | + # Button to choose file |
| 27 | + self.choose_button = tk.Button( |
| 28 | + self, text="Choose File", command=self.choose_file |
| 29 | + ) |
| 30 | + self.choose_button.pack(side=tk.RIGHT, anchor='n') |
| 31 | + |
| 32 | + # Frame for start preview |
| 33 | + self.start_frame_group = tk.Frame(self) |
| 34 | + self.start_frame_group.pack(side="left", padx=50) |
| 35 | + |
| 36 | + # Buttons to adjust start time |
| 37 | + self.start_dec_button = tk.Button( |
| 38 | + self.start_frame_group, text="<", command=self.decrease_start_time |
| 39 | + ) |
| 40 | + self.start_dec_button.grid(row=0, column=0) |
| 41 | + |
| 42 | + self.start_time_label = tk.Label( |
| 43 | + self.start_frame_group, text="Start: 0:00" |
| 44 | + ) |
| 45 | + self.start_time_label.grid(row=0, column=1) |
| 46 | + |
| 47 | + self.start_slider = ttk.Scale( |
| 48 | + self.start_frame_group, |
| 49 | + from_=0, |
| 50 | + to=0, |
| 51 | + length=220, |
| 52 | + orient="horizontal", |
| 53 | + variable=self.start_time, |
| 54 | + command=self.load_previews, |
| 55 | + ) |
| 56 | + self.start_slider.grid(row=1, column=0, columnspan=3, pady=10) |
| 57 | + |
| 58 | + self.start_inc_button = tk.Button( |
| 59 | + self.start_frame_group, text=">", command=self.increase_start_time |
| 60 | + ) |
| 61 | + self.start_inc_button.grid(row=0, column=2) |
| 62 | + |
| 63 | + self.preview_label_start = tk.Label(self.start_frame_group) |
| 64 | + self.preview_label_start.grid(row=2, column=0, columnspan=3) |
| 65 | + |
| 66 | + # Frame for end preview |
| 67 | + self.end_frame_group = tk.Frame(self) |
| 68 | + self.end_frame_group.pack(side="right", padx=50) |
| 69 | + |
| 70 | + # Buttons to adjust end time |
| 71 | + self.end_dec_button = tk.Button( |
| 72 | + self.end_frame_group, text="<", command=self.decrease_end_time |
| 73 | + ) |
| 74 | + self.end_dec_button.grid(row=0, column=0) |
| 75 | + |
| 76 | + self.end_time_label = tk.Label(self.end_frame_group, text="End: 0:00") |
| 77 | + self.end_time_label.grid(row=0, column=1) |
| 78 | + |
| 79 | + self.end_slider = ttk.Scale( |
| 80 | + self.end_frame_group, |
| 81 | + from_=0, |
| 82 | + to=0, |
| 83 | + length=220, |
| 84 | + orient="horizontal", |
| 85 | + variable=self.end_time, |
| 86 | + command=self.load_previews, |
| 87 | + ) |
| 88 | + self.end_slider.grid(row=1, column=0, columnspan=3, pady=10) |
| 89 | + |
| 90 | + self.end_inc_button = tk.Button( |
| 91 | + self.end_frame_group, text=">", command=self.increase_end_time |
| 92 | + ) |
| 93 | + self.end_inc_button.grid(row=0, column=2) |
| 94 | + |
| 95 | + self.preview_label_end = tk.Label(self.end_frame_group) |
| 96 | + self.preview_label_end.grid(row=2, column=0, columnspan=3) |
| 97 | + |
| 98 | + # Video capture objects |
| 99 | + self.video_capture_start = None |
| 100 | + self.video_capture_end = None |
| 101 | + self.total_frames = 0 |
| 102 | + |
| 103 | + def choose_file(self): |
| 104 | + self.file_path = filedialog.askopenfilename( |
| 105 | + filetypes=[ |
| 106 | + ('Video files', '*.mp4'), |
| 107 | + ('Video files', '*.m4v'), |
| 108 | + ('Video files', '*.ts')] |
| 109 | + ) |
| 110 | + if self.file_path: |
| 111 | + self.video_capture_start = cv2.VideoCapture(self.file_path) |
| 112 | + self.video_capture_end = cv2.VideoCapture(self.file_path) |
| 113 | + self.total_frames = int( |
| 114 | + self.video_capture_start.get(cv2.CAP_PROP_FRAME_COUNT) |
| 115 | + ) |
| 116 | + video_length = int( |
| 117 | + self.total_frames / self.video_capture_start.get( |
| 118 | + cv2.CAP_PROP_FPS |
| 119 | + ) |
| 120 | + ) |
| 121 | + self.start_time.set(0) |
| 122 | + self.end_time.set(video_length) |
| 123 | + self.start_slider.config(from_=0, to=video_length) |
| 124 | + self.end_slider.config(from_=0, to=video_length) |
| 125 | + self.load_previews() |
| 126 | + |
| 127 | + def load_previews(self): |
| 128 | + start_time_sec = int(self.start_time.get()) |
| 129 | + end_time_sec = int(self.end_time.get()) |
| 130 | + |
| 131 | + self.video_capture_start.set( |
| 132 | + cv2.CAP_PROP_POS_MSEC, start_time_sec * 1000 |
| 133 | + ) |
| 134 | + success_start, frame_start = self.video_capture_start.read() |
| 135 | + if success_start: |
| 136 | + frame_start = Image.fromarray( |
| 137 | + cv2.cvtColor(frame_start, cv2.COLOR_BGR2RGB) |
| 138 | + ) |
| 139 | + frame_start = frame_start.resize((300, 169)) |
| 140 | + frame_start = ImageTk.PhotoImage(frame_start) |
| 141 | + self.preview_label_start.configure(image=frame_start) |
| 142 | + self.preview_label_start.image = frame_start |
| 143 | + self.start_time_label.config( |
| 144 | + text=f"Start: {start_time_sec//60}:{start_time_sec%60:02d}" |
| 145 | + ) |
| 146 | + |
| 147 | + self.video_capture_end.set(cv2.CAP_PROP_POS_MSEC, end_time_sec * 1000) |
| 148 | + success_end, frame_end = self.video_capture_end.read() |
| 149 | + if success_end: |
| 150 | + frame_end = Image.fromarray( |
| 151 | + cv2.cvtColor(frame_end, cv2.COLOR_BGR2RGB) |
| 152 | + ) |
| 153 | + frame_end = frame_end.resize((300, 169)) |
| 154 | + frame_end = ImageTk.PhotoImage(frame_end) |
| 155 | + self.preview_label_end.configure(image=frame_end) |
| 156 | + self.preview_label_end.image = frame_end |
| 157 | + self.end_time_label.config( |
| 158 | + text=f"End: {end_time_sec//60}:{end_time_sec%60:02d}" |
| 159 | + ) |
| 160 | + |
| 161 | + def decrease_start_time(self): |
| 162 | + current_start_time = self.start_time.get() |
| 163 | + if current_start_time > 0: |
| 164 | + self.start_time.set(current_start_time - 1) |
| 165 | + self.load_previews() |
| 166 | + |
| 167 | + def increase_start_time(self): |
| 168 | + current_start_time = self.start_time.get() |
| 169 | + self.start_time.set(current_start_time + 1) |
| 170 | + self.load_previews() |
| 171 | + |
| 172 | + def decrease_end_time(self): |
| 173 | + current_end_time = self.end_time.get() |
| 174 | + if current_end_time > 0: |
| 175 | + self.end_time.set(current_end_time - 1) |
| 176 | + self.load_previews() |
| 177 | + |
| 178 | + def increase_end_time(self): |
| 179 | + current_end_time = self.end_time.get() |
| 180 | + self.end_time.set(current_end_time + 1) |
| 181 | + self.load_previews() |
| 182 | + |
| 183 | + def trim_video(self): |
| 184 | + start_time_sec = int(self.start_time.get()) |
| 185 | + end_time_sec = int(self.end_time.get()) |
| 186 | + |
| 187 | + # Get filename without extension |
| 188 | + output_file_name, output_file_extension = os.path.splitext( |
| 189 | + os.path.basename(self.file_path) |
| 190 | + ) |
| 191 | + output_path = os.path.splitext( |
| 192 | + os.path.dirname(self.file_path) |
| 193 | + )[0] |
| 194 | + output_file = ( |
| 195 | + f'{output_path}/{output_file_name}' |
| 196 | + f'_trimmed{output_file_extension}' |
| 197 | + ) |
| 198 | + |
| 199 | + # Execute FFMPEG command for trimming |
| 200 | + command = ( |
| 201 | + f'ffmpeg -i "{self.file_path}" ' |
| 202 | + f'-ss {start_time_sec} -to {end_time_sec} ' |
| 203 | + f'-c:v copy -c:a copy "{output_file}" -y' |
| 204 | + ) |
| 205 | + subprocess.call(command, shell=True) |
| 206 | + |
| 207 | + messagebox.showinfo("Success", "Video trimmed successfully!") |
| 208 | + |
| 209 | + |
| 210 | +if __name__ == "__main__": |
| 211 | + app = VideoTrimmerApp() |
| 212 | + app.mainloop() |
0 commit comments