begin vodkeeping
This commit is contained in:
+443
@@ -0,0 +1,443 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||
import threading
|
||||
import subprocess
|
||||
import os
|
||||
import json
|
||||
import psutil
|
||||
from datetime import datetime
|
||||
import cv2
|
||||
from PIL import Image, ImageTk
|
||||
import ffmpeg
|
||||
|
||||
class VodKeeper:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("VodKeeper - Video Processing Workflow")
|
||||
self.root.geometry("1200x800")
|
||||
|
||||
# Configuration
|
||||
self.config = {
|
||||
'input_path': 'R:\\mux\\input',
|
||||
'output_path': 'R:\\mux\\output',
|
||||
'logs_path': 'R:\\mux\\logs',
|
||||
'codec': 'h265_nvenc',
|
||||
'preset': 'p7',
|
||||
'cq': 0,
|
||||
'auto_bitrate': True,
|
||||
'preview_duration': 60
|
||||
}
|
||||
|
||||
# Render state
|
||||
self.current_process = None
|
||||
self.render_queue = []
|
||||
self.is_rendering = False
|
||||
|
||||
self.setup_ui()
|
||||
self.load_config()
|
||||
|
||||
def setup_ui(self):
|
||||
# Create notebook for tabs
|
||||
self.notebook = ttk.Notebook(self.root)
|
||||
self.notebook.pack(fill='both', expand=True, padx=10, pady=10)
|
||||
|
||||
# Input Tab
|
||||
self.setup_input_tab()
|
||||
|
||||
# Render Tab
|
||||
self.setup_render_tab()
|
||||
|
||||
# QC Tab
|
||||
self.setup_qc_tab()
|
||||
|
||||
def setup_input_tab(self):
|
||||
input_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(input_frame, text="Input")
|
||||
|
||||
# File selection
|
||||
file_frame = ttk.LabelFrame(input_frame, text="File Selection", padding=10)
|
||||
file_frame.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
ttk.Label(file_frame, text="Input Directory:").grid(row=0, column=0, sticky='w', pady=2)
|
||||
self.input_path_var = tk.StringVar(value=self.config['input_path'])
|
||||
ttk.Entry(file_frame, textvariable=self.input_path_var, width=50).grid(row=0, column=1, padx=5, pady=2)
|
||||
ttk.Button(file_frame, text="Browse", command=self.browse_input).grid(row=0, column=2, padx=5, pady=2)
|
||||
|
||||
# File list
|
||||
ttk.Label(file_frame, text="Available Files:").grid(row=1, column=0, sticky='w', pady=(10,2))
|
||||
self.file_listbox = tk.Listbox(file_frame, height=8, width=70)
|
||||
self.file_listbox.grid(row=2, column=0, columnspan=3, sticky='ew', pady=2)
|
||||
self.file_listbox.bind('<<ListboxSelect>>', self.on_file_select)
|
||||
|
||||
# Refresh button
|
||||
ttk.Button(file_frame, text="Refresh Files", command=self.refresh_files).grid(row=3, column=0, pady=5)
|
||||
|
||||
# Encoding settings
|
||||
settings_frame = ttk.LabelFrame(input_frame, text="Encoding Settings", padding=10)
|
||||
settings_frame.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
# Codec selection
|
||||
ttk.Label(settings_frame, text="Codec:").grid(row=0, column=0, sticky='w', pady=2)
|
||||
self.codec_var = tk.StringVar(value=self.config['codec'])
|
||||
codec_combo = ttk.Combobox(settings_frame, textvariable=self.codec_var,
|
||||
values=['h265_nvenc', 'vp9', 'av1'], state='readonly')
|
||||
codec_combo.grid(row=0, column=1, sticky='w', padx=5, pady=2)
|
||||
|
||||
# Preset selection
|
||||
ttk.Label(settings_frame, text="Preset:").grid(row=1, column=0, sticky='w', pady=2)
|
||||
self.preset_var = tk.StringVar(value=self.config['preset'])
|
||||
preset_combo = ttk.Combobox(settings_frame, textvariable=self.preset_var,
|
||||
values=['p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7'], state='readonly')
|
||||
preset_combo.grid(row=1, column=1, sticky='w', padx=5, pady=2)
|
||||
|
||||
# CQ setting
|
||||
ttk.Label(settings_frame, text="CQ:").grid(row=2, column=0, sticky='w', pady=2)
|
||||
self.cq_var = tk.IntVar(value=self.config['cq'])
|
||||
cq_spin = ttk.Spinbox(settings_frame, from_=0, to=51, textvariable=self.cq_var, width=10)
|
||||
cq_spin.grid(row=2, column=1, sticky='w', padx=5, pady=2)
|
||||
|
||||
# Bitrate settings
|
||||
bitrate_frame = ttk.LabelFrame(settings_frame, text="Bitrate Settings", padding=5)
|
||||
bitrate_frame.grid(row=3, column=0, columnspan=2, sticky='ew', pady=10)
|
||||
|
||||
self.auto_bitrate_var = tk.BooleanVar(value=self.config['auto_bitrate'])
|
||||
ttk.Checkbutton(bitrate_frame, text="Auto-detect bitrate cap",
|
||||
variable=self.auto_bitrate_var).grid(row=0, column=0, sticky='w')
|
||||
|
||||
ttk.Label(bitrate_frame, text="Manual bitrate cap (Mbps):").grid(row=1, column=0, sticky='w', pady=2)
|
||||
self.manual_bitrate_var = tk.DoubleVar(value=0)
|
||||
ttk.Entry(bitrate_frame, textvariable=self.manual_bitrate_var, width=10).grid(row=1, column=1, padx=5, pady=2)
|
||||
|
||||
# File info
|
||||
info_frame = ttk.LabelFrame(input_frame, text="File Information", padding=10)
|
||||
info_frame.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
self.file_info_text = scrolledtext.ScrolledText(info_frame, height=6, width=80)
|
||||
self.file_info_text.pack(fill='both', expand=True)
|
||||
|
||||
# Action buttons
|
||||
button_frame = ttk.Frame(input_frame)
|
||||
button_frame.pack(fill='x', padx=10, pady=10)
|
||||
|
||||
ttk.Button(button_frame, text="Add to Queue", command=self.add_to_queue).pack(side='left', padx=5)
|
||||
ttk.Button(button_frame, text="Preview (1 min)", command=self.preview_encode).pack(side='left', padx=5)
|
||||
ttk.Button(button_frame, text="Save Settings", command=self.save_config).pack(side='right', padx=5)
|
||||
|
||||
def setup_render_tab(self):
|
||||
render_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(render_frame, text="Render")
|
||||
|
||||
# Progress section
|
||||
progress_frame = ttk.LabelFrame(render_frame, text="Render Progress", padding=10)
|
||||
progress_frame.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
self.progress_var = tk.DoubleVar()
|
||||
self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100)
|
||||
self.progress_bar.pack(fill='x', pady=5)
|
||||
|
||||
self.progress_label = ttk.Label(progress_frame, text="Ready")
|
||||
self.progress_label.pack()
|
||||
|
||||
# Control buttons
|
||||
control_frame = ttk.Frame(progress_frame)
|
||||
control_frame.pack(pady=10)
|
||||
|
||||
self.start_button = ttk.Button(control_frame, text="Start Rendering", command=self.start_rendering)
|
||||
self.start_button.pack(side='left', padx=5)
|
||||
|
||||
self.pause_button = ttk.Button(control_frame, text="Pause", command=self.pause_rendering, state='disabled')
|
||||
self.pause_button.pack(side='left', padx=5)
|
||||
|
||||
self.stop_button = ttk.Button(control_frame, text="Stop", command=self.stop_rendering, state='disabled')
|
||||
self.stop_button.pack(side='left', padx=5)
|
||||
|
||||
# Queue section
|
||||
queue_frame = ttk.LabelFrame(render_frame, text="Render Queue", padding=10)
|
||||
queue_frame.pack(fill='both', expand=True, padx=10, pady=5)
|
||||
|
||||
self.queue_tree = ttk.Treeview(queue_frame, columns=('Status', 'Progress'), show='tree headings')
|
||||
self.queue_tree.heading('#0', text='File')
|
||||
self.queue_tree.heading('Status', text='Status')
|
||||
self.queue_tree.heading('Progress', text='Progress')
|
||||
self.queue_tree.pack(fill='both', expand=True)
|
||||
|
||||
# Log output
|
||||
log_frame = ttk.LabelFrame(render_frame, text="FFmpeg Output", padding=10)
|
||||
log_frame.pack(fill='both', expand=True, padx=10, pady=5)
|
||||
|
||||
self.log_text = scrolledtext.ScrolledText(log_frame, height=10)
|
||||
self.log_text.pack(fill='both', expand=True)
|
||||
|
||||
def setup_qc_tab(self):
|
||||
qc_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(qc_frame, text="QC")
|
||||
|
||||
# File selection for QC
|
||||
qc_file_frame = ttk.LabelFrame(qc_frame, text="QC File Selection", padding=10)
|
||||
qc_file_frame.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
ttk.Label(qc_file_frame, text="Source File:").grid(row=0, column=0, sticky='w', pady=2)
|
||||
self.qc_source_var = tk.StringVar()
|
||||
ttk.Entry(qc_file_frame, textvariable=self.qc_source_var, width=50).grid(row=0, column=1, padx=5, pady=2)
|
||||
ttk.Button(qc_file_frame, text="Browse", command=self.browse_qc_source).grid(row=0, column=2, padx=5, pady=2)
|
||||
|
||||
ttk.Label(qc_file_frame, text="Output File:").grid(row=1, column=0, sticky='w', pady=2)
|
||||
self.qc_output_var = tk.StringVar()
|
||||
ttk.Entry(qc_file_frame, textvariable=self.qc_output_var, width=50).grid(row=1, column=1, padx=5, pady=2)
|
||||
ttk.Button(qc_file_frame, text="Browse", command=self.browse_qc_output).grid(row=1, column=2, padx=5, pady=2)
|
||||
|
||||
# QC controls
|
||||
qc_control_frame = ttk.Frame(qc_frame)
|
||||
qc_control_frame.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
ttk.Button(qc_control_frame, text="Run QC Check", command=self.run_qc_check).pack(side='left', padx=5)
|
||||
ttk.Button(qc_control_frame, text="Compare Frames", command=self.compare_frames).pack(side='left', padx=5)
|
||||
ttk.Button(qc_control_frame, text="Export Report", command=self.export_qc_report).pack(side='left', padx=5)
|
||||
|
||||
# Frame comparison viewer
|
||||
comparison_frame = ttk.LabelFrame(qc_frame, text="Frame Comparison", padding=10)
|
||||
comparison_frame.pack(fill='both', expand=True, padx=10, pady=5)
|
||||
|
||||
# Frame navigation
|
||||
nav_frame = ttk.Frame(comparison_frame)
|
||||
nav_frame.pack(fill='x', pady=5)
|
||||
|
||||
ttk.Button(nav_frame, text="Previous Frame", command=self.prev_frame).pack(side='left', padx=5)
|
||||
self.frame_label = ttk.Label(nav_frame, text="Frame: 0")
|
||||
self.frame_label.pack(side='left', padx=20)
|
||||
ttk.Button(nav_frame, text="Next Frame", command=self.next_frame).pack(side='left', padx=5)
|
||||
|
||||
# Frame display
|
||||
display_frame = ttk.Frame(comparison_frame)
|
||||
display_frame.pack(fill='both', expand=True)
|
||||
|
||||
self.source_canvas = tk.Canvas(display_frame, bg='black')
|
||||
self.source_canvas.pack(side='left', fill='both', expand=True, padx=5)
|
||||
|
||||
self.output_canvas = tk.Canvas(display_frame, bg='black')
|
||||
self.output_canvas.pack(side='right', fill='both', expand=True, padx=5)
|
||||
|
||||
# QC results
|
||||
results_frame = ttk.LabelFrame(qc_frame, text="QC Results", padding=10)
|
||||
results_frame.pack(fill='x', padx=10, pady=5)
|
||||
|
||||
self.qc_results_text = scrolledtext.ScrolledText(results_frame, height=6)
|
||||
self.qc_results_text.pack(fill='both', expand=True)
|
||||
|
||||
def browse_input(self):
|
||||
path = filedialog.askdirectory(initialdir=self.config['input_path'])
|
||||
if path:
|
||||
self.input_path_var.set(path)
|
||||
self.refresh_files()
|
||||
|
||||
def refresh_files(self):
|
||||
self.file_listbox.delete(0, tk.END)
|
||||
input_path = self.input_path_var.get()
|
||||
if os.path.exists(input_path):
|
||||
for file in os.listdir(input_path):
|
||||
if file.lower().endswith(('.mp4', '.avi', '.mkv', '.mov', '.wmv')):
|
||||
self.file_listbox.insert(tk.END, file)
|
||||
|
||||
def on_file_select(self, event):
|
||||
selection = self.file_listbox.curselection()
|
||||
if selection:
|
||||
filename = self.file_listbox.get(selection[0])
|
||||
self.update_file_info(filename)
|
||||
|
||||
def update_file_info(self, filename):
|
||||
filepath = os.path.join(self.input_path_var.get(), filename)
|
||||
if os.path.exists(filepath):
|
||||
try:
|
||||
probe = ffmpeg.probe(filepath)
|
||||
video_info = next(s for s in probe['streams'] if s['codec_type'] == 'video')
|
||||
|
||||
info_text = f"File: {filename}\n"
|
||||
info_text += f"Duration: {float(probe['format']['duration']):.2f} seconds\n"
|
||||
info_text += f"Resolution: {video_info['width']}x{video_info['height']}\n"
|
||||
info_text += f"Codec: {video_info['codec_name']}\n"
|
||||
info_text += f"Bitrate: {int(probe['format']['bit_rate']) / 1000000:.2f} Mbps\n"
|
||||
|
||||
self.file_info_text.delete(1.0, tk.END)
|
||||
self.file_info_text.insert(1.0, info_text)
|
||||
|
||||
except Exception as e:
|
||||
self.file_info_text.delete(1.0, tk.END)
|
||||
self.file_info_text.insert(1.0, f"Error reading file: {str(e)}")
|
||||
|
||||
def add_to_queue(self):
|
||||
selection = self.file_listbox.curselection()
|
||||
if selection:
|
||||
filename = self.file_listbox.get(selection[0])
|
||||
self.render_queue.append({
|
||||
'filename': filename,
|
||||
'status': 'Queued',
|
||||
'progress': 0
|
||||
})
|
||||
self.update_queue_display()
|
||||
|
||||
def preview_encode(self):
|
||||
selection = self.file_listbox.curselection()
|
||||
if selection:
|
||||
filename = self.file_listbox.get(selection[0])
|
||||
self.encode_file(filename, preview=True)
|
||||
|
||||
def start_rendering(self):
|
||||
if self.render_queue and not self.is_rendering:
|
||||
self.is_rendering = True
|
||||
self.start_button.config(state='disabled')
|
||||
self.pause_button.config(state='normal')
|
||||
self.stop_button.config(state='normal')
|
||||
|
||||
# Start rendering thread
|
||||
render_thread = threading.Thread(target=self.render_queue_worker)
|
||||
render_thread.daemon = True
|
||||
render_thread.start()
|
||||
|
||||
def render_queue_worker(self):
|
||||
while self.render_queue and self.is_rendering:
|
||||
job = self.render_queue[0]
|
||||
self.encode_file(job['filename'])
|
||||
if self.render_queue:
|
||||
self.render_queue.pop(0)
|
||||
self.update_queue_display()
|
||||
|
||||
self.is_rendering = False
|
||||
self.start_button.config(state='normal')
|
||||
self.pause_button.config(state='disabled')
|
||||
self.stop_button.config(state='disabled')
|
||||
|
||||
def encode_file(self, filename, preview=False):
|
||||
input_path = os.path.join(self.input_path_var.get(), filename)
|
||||
output_filename = f"encoded_{filename}"
|
||||
output_path = os.path.join(self.config['output_path'], output_filename)
|
||||
|
||||
# Build FFmpeg command
|
||||
cmd = [
|
||||
'ffmpeg', '-i', input_path,
|
||||
'-c:v', self.codec_var.get(),
|
||||
'-preset', self.preset_var.get(),
|
||||
'-cq', str(self.cq_var.get())
|
||||
]
|
||||
|
||||
if preview:
|
||||
cmd.extend(['-t', str(self.config['preview_duration'])])
|
||||
|
||||
cmd.append(output_path)
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
universal_newlines=True, bufsize=1)
|
||||
|
||||
self.current_process = process
|
||||
|
||||
for line in process.stdout:
|
||||
self.log_text.insert(tk.END, line)
|
||||
self.log_text.see(tk.END)
|
||||
self.root.update_idletasks()
|
||||
|
||||
process.wait()
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Encoding failed: {str(e)}")
|
||||
|
||||
def pause_rendering(self):
|
||||
if self.current_process:
|
||||
self.current_process.terminate()
|
||||
|
||||
def stop_rendering(self):
|
||||
self.is_rendering = False
|
||||
if self.current_process:
|
||||
self.current_process.terminate()
|
||||
|
||||
def update_queue_display(self):
|
||||
for item in self.queue_tree.get_children():
|
||||
self.queue_tree.delete(item)
|
||||
|
||||
for i, job in enumerate(self.render_queue):
|
||||
self.queue_tree.insert('', 'end', text=job['filename'],
|
||||
values=(job['status'], f"{job['progress']}%"))
|
||||
|
||||
def browse_qc_source(self):
|
||||
filename = filedialog.askopenfilename(
|
||||
title="Select Source File",
|
||||
filetypes=[("Video files", "*.mp4 *.avi *.mkv *.mov *.wmv")]
|
||||
)
|
||||
if filename:
|
||||
self.qc_source_var.set(filename)
|
||||
|
||||
def browse_qc_output(self):
|
||||
filename = filedialog.askopenfilename(
|
||||
title="Select Output File",
|
||||
filetypes=[("Video files", "*.mp4 *.avi *.mkv *.mov *.wmv")]
|
||||
)
|
||||
if filename:
|
||||
self.qc_output_var.set(filename)
|
||||
|
||||
def run_qc_check(self):
|
||||
source = self.qc_source_var.get()
|
||||
output = self.qc_output_var.get()
|
||||
|
||||
if not source or not output:
|
||||
messagebox.showerror("Error", "Please select both source and output files")
|
||||
return
|
||||
|
||||
# Run QC check (placeholder for check_files.py functionality)
|
||||
self.qc_results_text.delete(1.0, tk.END)
|
||||
self.qc_results_text.insert(1.0, "QC check completed...\n")
|
||||
|
||||
def compare_frames(self):
|
||||
source = self.qc_source_var.get()
|
||||
output = self.qc_output_var.get()
|
||||
|
||||
if not source or not output:
|
||||
messagebox.showerror("Error", "Please select both source and output files")
|
||||
return
|
||||
|
||||
# Load frames for comparison
|
||||
self.load_comparison_frames()
|
||||
|
||||
def load_comparison_frames(self):
|
||||
# Placeholder for frame loading and display
|
||||
pass
|
||||
|
||||
def prev_frame(self):
|
||||
# Placeholder for frame navigation
|
||||
pass
|
||||
|
||||
def next_frame(self):
|
||||
# Placeholder for frame navigation
|
||||
pass
|
||||
|
||||
def export_qc_report(self):
|
||||
# Placeholder for QC report export
|
||||
pass
|
||||
|
||||
def save_config(self):
|
||||
self.config.update({
|
||||
'input_path': self.input_path_var.get(),
|
||||
'codec': self.codec_var.get(),
|
||||
'preset': self.preset_var.get(),
|
||||
'cq': self.cq_var.get(),
|
||||
'auto_bitrate': self.auto_bitrate_var.get()
|
||||
})
|
||||
|
||||
with open('vodkeeper_config.json', 'w') as f:
|
||||
json.dump(self.config, f, indent=2)
|
||||
|
||||
messagebox.showinfo("Success", "Configuration saved")
|
||||
|
||||
def load_config(self):
|
||||
try:
|
||||
with open('vodkeeper_config.json', 'r') as f:
|
||||
config = json.load(f)
|
||||
self.config.update(config)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
app = VodKeeper(root)
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user