Skip to content

Instantly share code, notes, and snippets.

@prespondek
Last active May 11, 2017 00:32
Show Gist options
  • Save prespondek/a2c50af02273d946aeede3e3c4955efa to your computer and use it in GitHub Desktop.
Save prespondek/a2c50af02273d946aeede3e3c4955efa to your computer and use it in GitHub Desktop.
Basic FFMpeg command line Frontend
######################################################################
#
# 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