aboutsummaryrefslogtreecommitdiff
path: root/vidslice/options.py
diff options
context:
space:
mode:
Diffstat (limited to 'vidslice/options.py')
-rw-r--r--vidslice/options.py313
1 files changed, 313 insertions, 0 deletions
diff --git a/vidslice/options.py b/vidslice/options.py
new file mode 100644
index 0000000..016fbda
--- /dev/null
+++ b/vidslice/options.py
@@ -0,0 +1,313 @@
+import typing
+from tkinter import *
+from tkinter import ttk
+
+HEADERS_ROW = 0
+START_ROW = 1
+END_ROW = 2
+DURATION_ROW = 3
+WIDTH_ROW = 4
+HEIGHT_ROW = 5
+FRAMERATE_ROW = 6
+CROP_TOP_ROW = 7
+CROP_BOTTOM_ROW = 8
+CROP_LEFT_ROW = 9
+CROP_RIGHT_ROW = 10
+LABEL_COL = 0
+ORIG_COL = 1
+EDIT_BOX_COL = 2
+NEW_COL = 3
+
+
+class FFmpegOptions:
+ def __init__(self, input, output, vf):
+ self.input = input
+ self.output = output
+ self.vf = vf
+
+ def output_with_vf(self):
+ if len(self.vf) > 0:
+ return self.output + ['-vf', ','.join(self.vf)]
+ else:
+ return self.output
+
+
+class Property:
+ def __init__(self, parent, name, row, convert: typing.Callable = int):
+ self.handlers = []
+ ttk.Label(parent, text=name).grid(column=LABEL_COL, row=row, sticky=(E, W))
+ self.orig = StringVar(parent, value="N/A")
+ ttk.Label(parent, textvariable=self.orig).grid(column=ORIG_COL, row=row, sticky=(E, W))
+ self.edit = BooleanVar(parent)
+ self.edit_widget = ttk.Checkbutton(parent, variable=self.edit, command=self.handle_edit)
+ self.edit_widget.grid(column=EDIT_BOX_COL, row=row, sticky=(E, W))
+ self.new = ttk.Spinbox(parent, command=self.handle_change)
+ self.new.state(['disabled'])
+ self.new.grid(column=NEW_COL, row=row, sticky=(E, W))
+ self.convert = convert
+ self.disable()
+
+ def disable(self):
+ self.enable(False)
+
+ def enable(self, enabled=True):
+ if enabled:
+ self.edit_widget.state(['!disabled'])
+ else:
+ self.edit.set(False)
+ self.edit_widget.state(['disabled'])
+ self.new.state(['disabled'])
+
+ def is_enabled(self):
+ return self.edit_widget.instate(['!disabled'])
+
+ def is_edit(self):
+ return self.edit.get()
+
+ def set_orig(self, val):
+ self.orig.set(str(val))
+
+ def get_orig(self):
+ return self.orig.get()
+
+ def set_calc_new(self, val):
+ if not self.is_edit():
+ if self.convert(self.new['from']) > val:
+ self.new.configure(from_=val)
+ if self.convert(self.new['to']) < val:
+ self.new.configure(to=val)
+ self.new.set(val)
+
+ def set_range(self, min, max):
+ self.new.configure(from_=min, to=max)
+
+ def get_final(self):
+ if len(self.new.get()) == 0:
+ return self.orig.get()
+ return self.new.get()
+
+ def handle_edit(self, *args):
+ if self.edit.get():
+ self.new.state(['!disabled'])
+ else:
+ self.new.state(['disabled'])
+ self.handle_change(None)
+
+ def on_change(self, callback):
+ self.handlers.append(callback)
+
+ def handle_change(self, *args):
+ for handler in self.handlers:
+ handler()
+
+
+class OptionsPanel(ttk.LabelFrame):
+ """
+ A Panel displaying ffmpeg options
+ """
+
+ def __init__(self, *args, **kw):
+ super(OptionsPanel, self).__init__(*args, text="Options", **kw)
+
+ def place_header(text, **kwargs):
+ ttk.Label(self, text=text, font='TkHeadingFont', justify='center', anchor='center'
+ ).grid(sticky=(E, W), **kwargs)
+
+ place_header("Field", column=LABEL_COL, row=HEADERS_ROW)
+ place_header("Original Value", column=ORIG_COL, row=HEADERS_ROW)
+ place_header("Edit?", column=EDIT_BOX_COL, row=HEADERS_ROW)
+ place_header("New Value", column=NEW_COL, row=HEADERS_ROW)
+
+ self.start_time = Property(self, "Start time (seconds)", START_ROW, float)
+ self.start_time.on_change(self.enforce_constraints)
+
+ self.end_time = Property(self, "End time (seconds)", END_ROW, float)
+ self.end_time.on_change(self.enforce_constraints)
+
+ self.duration = Property(self, "Duration (seconds)", DURATION_ROW, float)
+ self.duration.on_change(self.enforce_constraints)
+
+ self.width = Property(self, "Width", WIDTH_ROW, int)
+ self.width.on_change(self.enforce_constraints)
+
+ self.height = Property(self, "Height", HEIGHT_ROW, int)
+ self.height.on_change(self.enforce_constraints)
+
+ self.framerate = Property(self, "Framerate", FRAMERATE_ROW, float)
+ self.framerate.on_change(self.enforce_constraints)
+
+ self.crop_top = Property(self, "Crop Top", CROP_TOP_ROW, int)
+ self.crop_top.on_change(self.enforce_constraints)
+
+ self.crop_bottom = Property(self, "Crop Bottom", CROP_BOTTOM_ROW, int)
+ self.crop_bottom.on_change(self.enforce_constraints)
+
+ self.crop_left = Property(self, "Crop Left", CROP_LEFT_ROW, int)
+ self.crop_left.on_change(self.enforce_constraints)
+
+ self.crop_right = Property(self, "Crop Right", CROP_RIGHT_ROW, int)
+ self.crop_right.on_change(self.enforce_constraints)
+
+ for child in self.winfo_children():
+ child.grid_configure(padx=2, pady=2)
+
+ self.state(['disabled'])
+
+ def enforce_constraints(self):
+ self.start_time.enable()
+ self.end_time.enable()
+ self.duration.enable()
+ orig_start = float(self.start_time.get_orig())
+ orig_end = float(self.end_time.get_orig())
+ orig_duration = float(self.duration.get_orig())
+ if self.start_time.is_edit() and self.end_time.is_edit():
+ new_start = float(self.start_time.get_final())
+ new_end = float(self.end_time.get_final())
+ new_duration = new_end - new_start
+ self.start_time.set_range(orig_start, new_end)
+ self.end_time.set_range(new_start, orig_end)
+ self.duration.disable()
+ self.duration.set_calc_new(new_duration)
+ elif self.start_time.is_edit() and self.duration.is_edit():
+ new_start = float(self.start_time.get_final())
+ new_duration = float(self.duration.get_final())
+ new_end = new_start + new_duration
+ self.start_time.set_range(orig_start, orig_end - new_duration)
+ self.duration.set_range(0, orig_end - new_start)
+ self.end_time.disable()
+ self.end_time.set_calc_new(new_end)
+ elif self.end_time.is_edit() and self.duration.is_edit():
+ new_end = float(self.end_time.get_final())
+ new_duration = float(self.duration.get_final())
+ new_start = new_end - new_duration
+ self.end_time.set_range(orig_start + new_duration, orig_end)
+ self.duration.set_range(0, new_end - orig_start)
+ self.start_time.disable()
+ self.start_time.set_calc_new(new_start)
+ else:
+ new_start = float(self.start_time.get_final())
+ new_end = float(self.end_time.get_final())
+ new_duration = new_end - new_start
+ if self.duration.is_edit():
+ new_duration = float(self.duration.get_final())
+ new_end = new_start + new_duration
+ self.start_time.set_range(orig_start, orig_end)
+ self.end_time.set_range(orig_start, orig_end)
+ self.duration.set_range(0, orig_duration)
+ self.start_time.set_calc_new(new_start)
+ self.end_time.set_calc_new(new_end)
+ self.duration.set_calc_new(new_duration)
+
+ if self.width.is_enabled() and self.height.is_enabled():
+ orig_width = int(self.width.get_orig())
+ orig_height = int(self.height.get_orig())
+ new_width = int(self.width.get_final())
+ new_height = int(self.height.get_final())
+ self.width.set_range(1, 10 * orig_width)
+ self.height.set_range(1, 10 * orig_height)
+
+ self.width.set_calc_new(round(orig_width / orig_height * new_height))
+ self.height.set_calc_new(round(orig_height / orig_width * new_width))
+
+ self.crop_top.set_calc_new(0)
+ self.crop_top.set_range(0, int(self.height.get_final()) - int(self.crop_bottom.get_final()))
+ self.crop_bottom.set_calc_new(0)
+ self.crop_bottom.set_range(0, int(self.height.get_final()) - int(self.crop_top.get_final()))
+
+ self.crop_right.set_calc_new(0)
+ self.crop_right.set_range(0, int(self.width.get_final()) - int(self.crop_left.get_final()))
+ self.crop_left.set_calc_new(0)
+ self.crop_left.set_range(0, int(self.width.get_final()) - int(self.crop_right.get_final()))
+
+ if self.framerate.is_enabled():
+ orig_framerate = float(self.framerate.get_orig())
+ self.framerate.set_range(0, orig_framerate)
+ self.framerate.set_calc_new(orig_framerate)
+
+ def update_info(self, info):
+ import fractions
+
+ if info is None:
+ self.state(['disabled'])
+ for prop in [self.start_time, self.duration, self.end_time, self.width, self.height, self.framerate]:
+ prop.disable()
+ else:
+ start_time = float(info['format']['start_time'])
+ self.start_time.set_orig(start_time)
+
+ duration = float(info['format']['duration'])
+ self.duration.set_orig(duration)
+
+ self.end_time.set_orig(start_time + duration)
+
+ video_streams = [stream for stream in info['streams'] if
+ stream['codec_type'] == 'video' and stream['avg_frame_rate'] != '0/0']
+ if len(video_streams) > 0:
+ video_stream = video_streams[0]
+ self.width.enable()
+ self.width.set_orig(video_stream['width'])
+ self.height.enable()
+ self.height.set_orig(video_stream['height'])
+ self.crop_top.enable()
+ self.crop_top.set_orig(0)
+ self.crop_bottom.enable()
+ self.crop_bottom.set_orig(0)
+ self.crop_left.enable()
+ self.crop_left.set_orig(0)
+ self.crop_right.enable()
+ self.crop_right.set_orig(0)
+
+ framerate = round(float(fractions.Fraction(video_stream['avg_frame_rate'])), 3)
+ self.framerate.enable()
+ self.framerate.set_orig(framerate)
+ else:
+ self.width.disable()
+ self.height.disable()
+ self.framerate.disable()
+ self.crop_top.disable()
+ self.crop_bottom.disable()
+ self.crop_left.disable()
+ self.crop_right.disable()
+
+ self.state(['!disabled'])
+ self.enforce_constraints()
+
+ def ffmpeg_opts(self):
+ input_opts = []
+ output_opts = []
+ vf = []
+
+ if self.start_time.is_edit():
+ input_opts += ['-ss', str(self.start_time.get_final())]
+ elif self.end_time.is_edit() and self.duration.is_edit():
+ new_end = float(self.end_time.get_final())
+ new_duration = float(self.duration.get_final())
+ new_start = new_end - new_duration
+ input_opts += ['-ss', str(new_start)]
+ if self.end_time.is_edit():
+ input_opts += ['-to', str(self.end_time.get_final())]
+ if self.duration.is_edit():
+ output_opts += ['-t', str(self.duration.get_final())]
+
+ if self.width.is_edit() or self.height.is_edit():
+ width = str(self.width.get_final())
+ height = str(self.height.get_final())
+ if not self.width.is_edit():
+ width = "-1"
+ if not self.height.is_edit():
+ height = "-1"
+ vf += ['scale=' + width + ':' + height]
+
+ if self.crop_top.is_edit() or self.crop_bottom.is_edit() or \
+ self.crop_left.is_edit() or self.crop_right.is_edit():
+ out_w = int(self.width.get_final()) - int(self.crop_left.get_final()) - int(self.crop_right.get_final())
+ out_h = int(self.height.get_final()) - int(self.crop_top.get_final()) - int(self.crop_bottom.get_final())
+ vf += [f'crop={out_w}:{out_h}:{self.crop_left.get_final()}:{self.crop_top.get_final()}']
+
+ if self.framerate.is_edit():
+ output_opts += ['-r', str(self.framerate.get_final())]
+
+ return FFmpegOptions(input_opts, output_opts, vf)
+
+ def frame_count(self):
+ return float(self.duration.get_final()) * float(self.framerate.get_final())