오늘은 중간걸림돌이 였던 샤나 인코더를 대체할 프로그램을 찾다가.
Shutter Encoder라는 걸 찾았는데.. 뭐랄까.. 좀.. UI가 좋긴한데
내마음에 들지 않았다.. (중간에 멈추는 문제도 있고..)
인코딩을 하는것은.. 가끔하기는 하지만 중요한 문제이기도 해서..
차라리 파이썬으로 ffmpeg를 subprocess에서 출력만 로그창에 보이는걸로
차라리 파이썬으로 ffmpeg를 subprocess에서 출력만 로그창에 보이는걸로
처리하는게 훨씬 낫겠다.. 싶었다.. (역시 ffmpeg는 위대하다..)
어짜피 대부분 사용자는 인코딩 프리셋을 한번가져가면 많이 안바뀌니까..
그래서 그걸 사용하려 한다. 이름은 가나 인코더.. -_-;;
실행폴더에.. presets.json 파일안에 다음처럼 ffmpeg에 들어가는 옵션을 집어넣었고
{
"H.264 - CRF 23 (Standard)": [
"-c:v", "h264_nvenc",
"-preset", "p4",
"-crf", "23",
"-c:a", "aac",
"-b:a", "128k"
],
"H.264 - CRF 20 (High Quality)": [
"-c:v", "h264_nvenc",
"-preset", "p4",
"-crf", "20",
"-c:a", "aac",
"-b:a", "192k"
],
"H.264 - Fast (Draft)": [
"-c:v", "h264_nvenc",
"-preset", "p1",
"-crf", "26",
"-c:a", "aac",
"-b:a", "96k"
]
}
내용은 스텐다드와 고퀄 하옵.. 이렇게 3개 세킹 갖춰넣고.. 음질은 aac 로 128k를
기본 셋업으로 했고 엔비디아 gpu 가속을 받는 옵션으로 했다.
기본 셋업으로 했고 엔비디아 gpu 가속을 받는 옵션으로 했다.
import json
import os
import subprocess
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk
class ShanaLikeEncoder:
def __init__(self, root):
self.root = root
self.root.title("Gana MP4 Encoder")
self.root.geometry("900x560")
self.presets = self.load_presets()
# 프로세스 및 상태 제어를 위한 변수
self.current_process = None
self.is_running = False
self.create_widgets()
def load_presets(self):
try:
with open("presets.json", "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return {
"Standard MP4": [
"-c:v",
"libx264",
"-crf",
"23",
"-c:a",
"aac",
]
}
def create_widgets(self):
# 상단: 옵션 및 제어 프레임
top_frame = tk.Frame(self.root, padx=10, pady=10)
top_frame.pack(fill=tk.X)
tk.Label(top_frame, text="프리셋:").pack(side=tk.LEFT, padx=(0, 5))
self.preset_combobox = ttk.Combobox(
top_frame,
values=list(self.presets.keys()),
state="readonly",
width=20,
)
if self.presets:
self.preset_combobox.current(0)
self.preset_combobox.pack(side=tk.LEFT, padx=(0, 15))
tk.Label(top_frame, text="해상도 제한:").pack(side=tk.LEFT, padx=(0, 5))
self.res_options = {
"원본 유지 (Keep Original)": None,
"최대 1080p (FHD)": "scale='min(1920,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease,pad=ceil(iw/2)*2:ceil(ih/2)*2",
"최대 720p (HD)": "scale='min(1280,iw)':'min(720,ih)':force_original_aspect_ratio=decrease,pad=ceil(iw/2)*2:ceil(ih/2)*2",
}
self.res_combobox = ttk.Combobox(
top_frame,
values=list(self.res_options.keys()),
state="readonly",
width=18,
)
self.res_combobox.current(0)
self.res_combobox.pack(side=tk.LEFT, padx=(0, 15))
# 시작 / 중지 버튼
self.start_btn = tk.Button(
top_frame,
text="인코딩 시작",
command=self.start_encoding_thread,
bg="#4CAF50",
fg="white",
width=10,
)
self.start_btn.pack(side=tk.LEFT, padx=(0, 5))
self.stop_btn = tk.Button(
top_frame,
text="중지",
command=self.stop_encoding,
bg="#f44336",
fg="white",
width=8,
state=tk.DISABLED,
)
self.stop_btn.pack(side=tk.LEFT)
# 중앙: 좌우 분할 컨테이너 (약 35:65 비율)
main_pane = tk.PanedWindow(self.root, orient=tk.HORIZONTAL, sashrelief=tk.RAISED)
main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
# 왼쪽: 대상 파일 목록 프레임 (35%)
left_frame = tk.Frame(main_pane)
main_pane.add(left_frame, width=315)
list_ctrl_frame = tk.Frame(left_frame)
list_ctrl_frame.pack(fill=tk.X, pady=(0, 5))
self.add_btn = tk.Button(list_ctrl_frame, text="파일 추가", command=self.add_files)
self.add_btn.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 2))
self.del_btn = tk.Button(list_ctrl_frame, text="선택 삭제", command=self.remove_selected)
self.del_btn.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(2, 0))
# 파일 리스트박스와 스크롤바
list_container = tk.Frame(left_frame)
list_container.pack(fill=tk.BOTH, expand=True)
self.file_listbox = tk.Listbox(
list_container, selectmode=tk.EXTENDED, bg="#2b2b2b", fg="#ffffff"
)
list_scroll = tk.Scrollbar(
list_container, orient=tk.VERTICAL, command=self.file_listbox.yview
)
self.file_listbox.configure(yscrollcommand=list_scroll.set)
self.file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
list_scroll.pack(side=tk.RIGHT, fill=tk.Y)
# 오른쪽: 로그 출력 텍스트박스 (65%)
right_frame = tk.Frame(main_pane)
main_pane.add(right_frame, width=585)
self.log_area = scrolledtext.ScrolledText(
right_frame, wrap=tk.WORD, bg="#1e1e1e", fg="#00ff00", insertbackground="white"
)
self.log_area.pack(fill=tk.BOTH, expand=True)
def add_files(self):
file_paths = filedialog.askopenfilenames(
filetypes=[("Video Files", "*.mp4 *.mkv *.avi *.mov *.webm"), ("All Files", "*.*")]
)
for path in file_paths:
if path not in self.file_listbox.get(0, tk.END):
self.file_listbox.insert(tk.END, path)
def remove_selected(self):
if self.is_running:
return
selected_indices = self.file_listbox.curselection()
for index in reversed(selected_indices):
self.file_listbox.delete(index)
def start_encoding_thread(self):
files = self.file_listbox.get(0, tk.END)
if not files:
messagebox.showerror("오류", "변환할 파일을 하나 이상 추가해주세요.")
return
# 상태 변경 (실행 중)
self.is_running = True
self.start_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.NORMAL)
self.add_btn.config(state=tk.DISABLED)
self.del_btn.config(state=tk.DISABLED)
self.preset_combobox.config(state=tk.DISABLED)
self.res_combobox.config(state=tk.DISABLED)
threading.Thread(
target=self.run_batch_encoding, args=(files,), daemon=True
).start()
def run_batch_encoding(self, files):
selected_preset = self.preset_combobox.get()
preset_args = self.presets.get(selected_preset, [])
selected_res_name = self.res_combobox.get()
filter_arg = self.res_options.get(selected_res_name)
total_files = len(files)
for idx, input_file in enumerate(files, 1):
if not self.is_running:
break
if not os.path.exists(input_file):
continue
base, _ = os.path.splitext(input_file)
output_file = f"{base}_encoded.mp4"
cmd = ["ffmpeg", "-y", "-i", input_file] + preset_args
if filter_arg:
cmd.extend(["-vf", filter_arg])
cmd.append(output_file)
self.log_area.insert(
tk.END,
f"[{idx}/{total_files}] 변환 중: {os.path.basename(input_file)}\n"
f"실행 명령어: {' '.join(cmd)}\n" + "-" * 50 + "\n",
)
self.log_area.see(tk.END)
self.current_process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
encoding="utf-8",
errors="replace",
)
# 프로세스 출력을 실시간으로 읽음 (중지 시그널 대응)
while True:
line = self.current_process.stdout.readline()
if not line and self.current_process.poll() is not None:
break
if line:
self.log_area.insert(tk.END, line)
self.log_area.see(tk.END)
returncode = self.current_process.poll()
self.current_process = None
if not self.is_running:
self.log_area.insert(tk.END, "\n[중지됨] 사용자에 의해 작업이 중단되었습니다.\n\n")
break
if returncode != 0:
self.log_area.insert(
tk.END, f"\n[오류] 파일 변환 실패 (코드: {returncode})\n\n"
)
else:
self.log_area.insert(tk.END, f"\n[완료] 성공: {output_file}\n\n")
if self.is_running:
self.log_area.insert(tk.END, "[전체 작업 완료] 모든 파일의 인코딩이 끝났습니다.\n")
self.log_area.see(tk.END)
self.reset_ui_state()
def stop_encoding(self):
if self.is_running:
self.is_running = False
if self.current_process:
try:
self.current_process.terminate()
self.current_process.wait(timeout=2)
except subprocess.TimeoutExpired:
self.current_process.kill()
except Exception:
pass
self.log_area.insert(tk.END, "\n[알림] 중지 버튼이 눌렸습니다. 현재 파일을 정리하는 중...\n")
self.log_area.see(tk.END)
def reset_ui_state(self):
self.is_running = False
self.current_process = None
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
self.add_btn.config(state=tk.NORMAL)
self.del_btn.config(state=tk.NORMAL)
self.preset_combobox.config(state="readonly")
self.res_combobox.config(state="readonly")
if __name__ == "__main__":
root = tk.Tk()
app = ShanaLikeEncoder(root)
root.mainloop()
요로코롬
댓글 없음:
댓글 쓰기