Last active
May 11, 2017 00:32
-
-
Save prespondek/a2c50af02273d946aeede3e3c4955efa to your computer and use it in GitHub Desktop.
Basic FFMpeg command line Frontend
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
###################################################################### | |
# | |
# FFMpeg command line frontend using python and TKinter. | |
# | |
# Requires Python 2.7 | |
# | |
# This is not an "easy mode" for ffmpeg. It's just a labour | |
# saving device for transcoding video files. You will need to download | |
# and install FFMpeg from http://ffmpeg.org and add ffmpeg as an | |
# environment variable. It's pretty basic right now but if you have | |
# some knowledge of python you should be able to extend it easily. | |
# | |
# Written by Peter Respondek | |
# | |
from Tkinter import * | |
from threading import Thread | |
import tkMessageBox | |
import os | |
from os import listdir | |
from os.path import isfile, join | |
import tkFileDialog | |
import subprocess | |
import Queue | |
import signal | |
class Application(Frame): | |
# Does the equvalient of pressing the 'q' at ffmpeg command line | |
def stopEncode(self): | |
if self.process and not self.process.poll(): | |
self.stopped = True | |
self.process.communicate(input='q') | |
# Opens ffmpeg process and encodes. Threaded. | |
def doEncode(self,params): | |
self.encoding = True | |
self.process = subprocess.Popen(params,stdin=subprocess.PIPE, stdout=subprocess.PIPE,stderr=subprocess.STDOUT, universal_newlines=True, bufsize=1, startupinfo=self.startupInfo()) | |
for line in iter(self.process.stdout.readline, ''): | |
self.queue.put(line) | |
self.process.stdout.close() | |
self.error = self.process.wait() | |
# these subprocess startup flags stops empty console window from poping up. | |
def startupInfo(self): | |
startupinfo = subprocess.STARTUPINFO() | |
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW | |
return startupinfo | |
# toggle buttons between encoding and stopped state | |
def toggleButtons(self,ready): | |
if ready: | |
self.encode_button.config(state="active", relief=RAISED) | |
self.stop_button.config(state="disabled", relief=SUNKEN) | |
if self.listbox.size(): | |
self.listbox.itemconfig(0, {'bg':'white'}) | |
else: | |
self.encode_button.config(state="disabled", relief=SUNKEN) | |
self.stop_button.config(state="active", relief=RAISED) | |
if self.listbox.size(): | |
self.listbox.itemconfig(0, {'bg':'red'}) | |
# called once encode button is pressed. Done some checking | |
def encode(self): | |
if not self.ffmpeg_installed: | |
tkMessageBox.showerror("Error", "FFMpeg not installed") | |
self.encoding = False | |
return | |
if not self.files: | |
tkMessageBox.showerror("Error", "No source files") | |
self.encoding = False | |
return | |
self.encode_file = self.files[0] | |
if not os.path.isfile(self.encode_file): | |
tkMessageBox.showerror("Error", "Source file does not exist") | |
self.encoding = False | |
return | |
if not self.idir or not os.path.isdir(self.idir): | |
self.encoding = False | |
tkMessageBox.showerror("Error", "No destination directory") | |
return | |
idir,ifile = os.path.split(self.encode_file) | |
ifile,iext = os.path.splitext(ifile) | |
opath = os.path.normpath(os.path.join(self.dest_directory.get(),ifile + self.dest_ext.get())) | |
if opath == self.encode_file: | |
self.encoding = False | |
tkMessageBox.showerror("Error", "Source and destinations filenames are the same.") | |
return | |
if os.path.isfile(opath): | |
output = tkMessageBox.askyesno("Error", opath + "\nDestination file already exists. Do you want to replace the file?") | |
if (output): | |
os.remove(opath) | |
else: | |
self.encoding = False | |
return | |
params = ['ffmpeg', '-hide_banner'] | |
params.extend(self.input_options.get().split()) | |
params.extend(['-i', self.encode_file]) | |
if self.vcodec.get() != "none": | |
params.extend(["-vcodec", self.vcodec.get()]) | |
if self.acodec.get() != "none": | |
params.extend(["-acodec", self.acodec.get()]) | |
if self.scodec.get() != "none": | |
params.extend(["-scodec", self.scodec.get()]) | |
params.extend(self.output_options.get().split()) | |
params.append(opath) | |
print "FFMpeg params: " + str(params) | |
self.console.delete(1.0,END) | |
self.toggleButtons(False) | |
self.t = Thread(target=self.doEncode,args=(params,)) | |
self.t.start() | |
# called when "add directory" button is pressed. Add all files in dir | |
def sourceDirectoryDialog(self): | |
options = {} | |
options["initialdir"] = self.odir | |
temp = tkFileDialog.askdirectory(**options) | |
if (temp): | |
self.odir = temp | |
onlyfiles = [f for f in listdir(self.odir) if isfile(join(self.odir, f))] | |
for ofile in onlyfiles: | |
self.files.append(os.path.normpath(os.path.join(self.odir, ofile))) | |
self.listbox.insert(END,ofile) | |
# called when "add file" button is pressed. | |
def sourceFileDialog(self): | |
options = {} | |
options["initialdir"] = self.odir | |
self.odir = tkFileDialog.askopenfilename(**options) | |
self.files.append(os.path.normpath(self.odir)) | |
temp = os.path.split(self.odir) | |
self.odir = temp[0] | |
self.listbox.insert(END,temp[1]) | |
# called when "remove file" button is pressed. | |
def sourceFileRemove(self): | |
idx = self.listbox.curselection()[0] | |
if idx == 0 and self.process and not self.process.poll(): | |
return | |
self.listbox.delete(idx) | |
self.files.pop(idx) | |
pass | |
# called when destination directory is added. | |
def destinationDirectoryDialog(self): | |
options = {} | |
options["initialdir"] = self.idir | |
self.idir = tkFileDialog.askdirectory(**options) | |
self.dest_directory.set(self.idir) | |
def fileSelected(self,event): | |
if self.process and not self.process.poll(): | |
return | |
if self.listbox.curselection() and self.files[self.listbox.curselection()[0]] and self.ffmpeg_installed: | |
io = subprocess.check_output(['ffprobe', '-hide_banner', self.files[self.listbox.curselection()[0]]], stderr=subprocess.STDOUT, shell=True) | |
self.console.delete(1.0,END) | |
self.console.insert(END,io) | |
# constantly check queue and update console as needed. | |
def updateOutput(self): | |
try: | |
while 1: | |
line = self.queue.get_nowait() | |
if line is None: | |
pass | |
else: | |
self.console.insert(END, str(line)) | |
self.console.see(END) | |
self.console.update_idletasks() | |
except Queue.Empty: | |
pass | |
# if thread is not longer alive we can move onto the next job | |
if not self.t.isAlive() and self.encoding: | |
if self.error != 0 or self.stopped == True: | |
self.reset() | |
else: | |
if len(self.files): | |
self.listbox.delete(0) | |
self.files.pop(0) | |
if len(self.files): | |
self.encode() | |
else: | |
self.reset() | |
self.after(100, self.updateOutput) | |
def reset(self): | |
self.encoding = False | |
self.stopped = False | |
self.process = False | |
self.error = 0 | |
self.toggleButtons(True) | |
#create Source GUI elements | |
def createSourceWidgets(self): | |
frame = LabelFrame(self, text="Source") | |
frame.grid(row=0,padx=5, columnspan = 2, pady=5, sticky=N+S+E+W) | |
Grid.columnconfigure(frame, 0, weight=1) | |
bframe = Frame(frame) | |
bframe.grid(row=0, columnspan = 2, sticky=N+S+E+W) | |
button = Button(bframe, text = "Add File", command = self.sourceFileDialog) | |
button.grid(row=0,column=0,padx=5,pady=5, sticky=N+S+E+W) | |
button = Button(bframe, text = "Add Directory", command = self.sourceDirectoryDialog) | |
button.grid(row=0,column=1,padx=5,pady=5, sticky=N+S+E+W) | |
button = Button(bframe, text = "Remove File", command = self.sourceFileRemove) | |
button.grid(row=0,column=2,padx=5,pady=5, sticky=N+S+E+W) | |
Grid.columnconfigure(bframe, 0, weight=1) | |
Grid.columnconfigure(bframe, 1, weight=1) | |
Grid.columnconfigure(bframe, 2, weight=1) | |
self.listbox = Listbox(frame) | |
self.listbox.bind("<ButtonRelease-1>", self.fileSelected) | |
self.listbox.bind("<Up>", self.fileSelected) | |
self.listbox.bind("<Down>", self.fileSelected) | |
self.listbox.grid(row=1, padx=(5,0), pady=5, sticky=N+S+E+W) | |
Grid.rowconfigure(frame, 1, weight=1) | |
scrollbar = Scrollbar(frame) | |
scrollbar.grid(row=1,column=1,padx=(0,5),pady=5, sticky=N+S+E+W) | |
self.listbox.config(yscrollcommand=scrollbar.set) | |
scrollbar.config(command=self.listbox.yview) | |
#create Destination GUI elements | |
def createDestinationWidgets(self): | |
frame = LabelFrame(self, text="Destination") | |
frame.grid(row=1,padx=5, columnspan = 2, pady=5, sticky=N+S+E+W) | |
label = Label(frame, text="Destination Directory:") | |
label.grid(row=0,column=0,padx=5,pady=5) | |
self.dest_directory = StringVar() | |
label = Entry(frame, textvariable=self.dest_directory) | |
label.grid(row=0,column=1, padx=5, pady=5, sticky=N+S+E+W) | |
Grid.columnconfigure(frame, 1, weight=1) | |
button = Button(frame, text = "Select", command = self.destinationDirectoryDialog) | |
button.grid(row=0,column=2,padx=5,pady=5, sticky=N+S+E+W) | |
label = Label(frame, text="Extension:") | |
label.grid(row=0,column=3,padx=5,pady=5) | |
self.dest_ext = StringVar() | |
self.dest_ext.set(".mp4") | |
label = Entry(frame, textvariable=self.dest_ext) | |
label.grid(row=0,column=4, padx=5, pady=5, sticky=N+S+E+W) | |
#Grid.columnconfigure(frame, 4, weight=1) | |
# get codecs from ffmpeg and parse the result | |
def getFFMpegCodecs(self): | |
# get all supported codecs from ffmpeg | |
try: | |
io = subprocess.check_output(['ffmpeg', '-hide_banner', '-encoders'], startupinfo=self.startupInfo()) | |
except: | |
self.console.tag_config("warn", foreground="red") | |
self.console.insert(END,"\n\nFFMPEG NOT FOUND!!!!\nPlease install and restart this script", "warn") | |
return (["none",],["none",],["none",]) | |
finally: | |
self.ffmpeg_installed = True | |
self.console.tag_config("warn", foreground="dark green") | |
self.console.insert(END,"\n\nFFMPEG FOUND!!!!", "warn") | |
codecs = (re.findall("(?<=V.....\s)\w+",io), \ | |
re.findall("(?<=A.....\s)\w+",io), \ | |
re.findall("(?<=S.....\s)\w+",io)) | |
for codec in codecs: | |
codec.append("copy") | |
codec.append("none") | |
return codecs | |
# if you want to extend this scripts functionality this is probably where you want to do it | |
def createOptionWidgets(self): | |
codecs = self.getFFMpegCodecs() | |
oframe = Frame(self) | |
oframe.grid(row=2, columnspan=2, sticky=N+S+E+W) | |
Grid.columnconfigure(oframe, 0, weight=1) | |
Grid.columnconfigure(oframe, 1, weight=1) | |
Grid.columnconfigure(oframe, 2, weight=1) | |
vframe = LabelFrame(oframe, text="Video Encoder") | |
vframe.grid(row=0,padx=5, pady=5, sticky=N+S+E+W) | |
self.vcodec = StringVar() | |
self.vcodec.set("libx264") | |
menu1 = apply(OptionMenu, (vframe, self.vcodec) + tuple(codecs[0])) | |
menu1.grid(padx=5,pady=5, sticky=N+S+E+W) | |
Grid.columnconfigure(vframe, 0, weight=1) | |
aframe = LabelFrame(oframe, text="Audio Encoder") | |
aframe.grid(row=0,column=1,padx=5, pady=5, sticky=N+S+E+W) | |
self.acodec = StringVar() | |
self.acodec.set("aac") | |
menu2 = apply(OptionMenu, (aframe, self.acodec) + tuple(codecs[1])) | |
menu2.grid(padx=5,pady=5, sticky=N+S+E+W) | |
Grid.columnconfigure(aframe, 0, weight=1) | |
sframe = LabelFrame(oframe, text="Subtitle Encoder") | |
sframe.grid(row=0,column=2,padx=5, pady=5, sticky=N+S+E+W) | |
self.scodec = StringVar() | |
self.scodec.set("none") | |
menu3 = apply(OptionMenu, (sframe, self.scodec) + tuple(codecs[2])) | |
menu3.grid(padx=5,pady=5, sticky=N+S+E+W) | |
Grid.columnconfigure(sframe, 0, weight=1) | |
oframe = LabelFrame(self, text="Additional FFMpeg Options (Advanced)") | |
oframe.grid(row=3,column=0, columnspan=3, padx=5, pady=5, sticky=N+S+E+W) | |
Grid.columnconfigure(oframe, 1, weight=1) | |
Grid.columnconfigure(oframe, 3, weight=1) | |
label = Label(oframe, text="Input:") | |
label.grid(row=0,column=0,padx=5,pady=5) | |
self.input_options = StringVar() | |
label = Entry(oframe, textvariable=self.input_options) | |
label.grid(row=0,column=1,padx=5, pady=5, sticky=N+S+E+W) | |
label = Label(oframe, text="Output:") | |
label.grid(row=0,column=2,padx=5,pady=5) | |
self.output_options = StringVar() | |
label = Entry(oframe, textvariable=self.output_options) | |
label.grid(row=0,column=3,padx=5, pady=5, sticky=N+S+E+W) | |
#create Output GUI elements | |
def createOutputWidgets(self): | |
frame = LabelFrame(self, text="Output") | |
Grid.columnconfigure(frame, 0, weight=1) | |
frame.grid(row=4,padx=5, columnspan = 2, pady=5, sticky=N+S+E+W) | |
self.console = Text(frame, wrap=NONE) | |
self.console.grid(row=0,padx=(5,0),pady=(5,0), sticky=N+S+E+W) | |
self.console.tag_config("intro", foreground="blue") | |
self.console.insert(END,"IMPORTANT!: \n\n" +\ | |
"This script requires ffmpeg command line tool available at:\n" +\ | |
"http://ffmpeg.org\n" +\ | |
"Furthermore it requires that ffmpeg is added to your environment variables","intro") | |
scrollbar = Scrollbar(frame) | |
scrollbar.grid(row=0,column=1,padx=(0,5),pady=5, sticky=N+S+E+W) | |
self.console.config(yscrollcommand=scrollbar.set) | |
scrollbar.config(command=self.console.yview) | |
scrollbar = Scrollbar(frame, orient=HORIZONTAL) | |
scrollbar.grid(row=1,column=0,padx=(5,0),pady=(0,5), sticky=N+S+E+W) | |
self.console.config(xscrollcommand=scrollbar.set) | |
scrollbar.config(command=self.console.xview) | |
Grid.rowconfigure(frame, 0, weight=1) | |
#create Encode State Buttons | |
def createEncodeButtons(self): | |
self.encode_button = Button(self, text = "ENCODE", relief=RAISED, command = self.encode) | |
self.encode_button.grid(row=5, padx=5, pady=5, sticky=N+S+E+W) | |
self.stop_button = Button(self, text = "STOP", state="disabled", relief=SUNKEN, command = self.stopEncode) | |
self.stop_button.grid(row=5, column=1, padx=5, pady=5, sticky=N+S+E+W) | |
def shutdown(self): | |
if self.process and not self.process.poll(): | |
try: | |
self.stopEncode() | |
except: | |
pass | |
root.destroy() | |
def __init__(self, master=None): | |
Frame.__init__(self, master) | |
self.files = [] | |
self.odir = "" | |
self.idir = "" | |
self.process = False | |
self.ffmpeg_installed = False | |
self.encoding = False | |
self.stopped = False | |
self.error = 0 | |
self.t = Thread(target=self.doEncode) | |
self.grid(sticky=N+S+E+W) | |
# I use a queue here because Tkinter is not thread safe we need to periodically send the output window updates from our encode thread. | |
self.queue = Queue.Queue() | |
# setup our GUI | |
self.createSourceWidgets() | |
self.createDestinationWidgets() | |
self.createOutputWidgets() | |
self.createEncodeButtons() | |
self.createOptionWidgets() | |
Grid.rowconfigure(self, 0, weight=1, minsize=120) | |
Grid.rowconfigure(self, 1, weight=1, minsize=70) | |
Grid.rowconfigure(self, 2, weight=1, minsize=70) | |
Grid.rowconfigure(self, 3, weight=1, minsize=70) | |
Grid.rowconfigure(self, 4, weight=1) | |
Grid.columnconfigure(self, 0, weight=1) | |
Grid.columnconfigure(self, 1, weight=1) | |
# this constantly checks our queue for updates | |
self.updateOutput() | |
root = Tk() | |
root.title("FFMpeg Helper") | |
root.minsize(600,600) | |
Grid.rowconfigure(root, 0, weight=1) | |
Grid.columnconfigure(root, 0, weight=1) | |
app = Application(master=root) | |
root.protocol("WM_DELETE_WINDOW", app.shutdown) | |
app.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment