commit 6927b5c457d69ea263adc17be8793d051d0d912c Author: Patrick Gniza Date: Mon May 18 15:37:35 2026 +0200 erster commit diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..bf378a9 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,30 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build Grabber Config", + "type": "shell", + "command": "${workspaceFolder}/.venv/Scripts/python.exe", + "args": [ + "-m", "nuitka", + "--standalone", + "--enable-plugin=multiprocessing", + "--enable-plugin=anti-bloat", + "--enable-plugin=tk-inter", + "--lto=no", + "--msvc=latest", + "--output-dir=build", + "--assume-yes-for-downloads", + "--windows-company-name=Patrick Gniza", + "--windows-product-name=Grabber Config", + "--windows-file-version=1.0.0", + "--windows-product-version=1.0.0", + "--windows-file-description=Grabber Config", + "--windows-icon-from-ico=GrabberConfig.ico", + "GrabberConfig.py" + ], + "group": "build", + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/GrabberConfig.ico b/GrabberConfig.ico new file mode 100644 index 0000000..5216452 Binary files /dev/null and b/GrabberConfig.ico differ diff --git a/GrabberConfig.py b/GrabberConfig.py new file mode 100644 index 0000000..b2f07e6 --- /dev/null +++ b/GrabberConfig.py @@ -0,0 +1,658 @@ +import argparse +import json +import os +import sys +import tkinter as tk +from tkinter import ttk, messagebox +from tkinter.scrolledtext import ScrolledText + +import comtypes +import comtypes.client +from comtypes import GUID, COMMETHOD, HRESULT +from comtypes.automation import IUnknown, VARIANT +from ctypes import c_ulong, c_long, POINTER, byref + + +PROGRAMDATA_DIR = os.path.join( + os.environ.get("PROGRAMDATA", r"C:\ProgramData"), + "GrabberConfig" +) + +CONFIG_FILE = os.path.join(PROGRAMDATA_DIR, "config.json") + +def ensure_config_dir(): + os.makedirs(PROGRAMDATA_DIR, exist_ok=True) + +CLSID_SystemDeviceEnum = GUID("{62BE5D10-60EB-11d0-BD3B-00A0C911CE86}") +CLSID_VideoInputDeviceCategory = GUID("{860BB310-5D01-11d0-BD3B-00A0C911CE86}") +CLSID_AM_KSCATEGORY_CROSSBAR = GUID("{A799A801-A46D-11D0-A18C-00A02401DCD4}") + +IID_IBaseFilter = GUID("{56A86895-0AD4-11CE-B03A-0020AF0BA770}") + + +PHYS_CONN_NAMES = { + 1: "Video Tuner", + 2: "Composite", + 3: "S-Video", + 4: "RGB", + 5: "YRYBY", + 6: "Serial Digital", + 7: "Parallel Digital", + 8: "SCSI", + 9: "AUX", + 10: "1394", + 11: "USB", + 12: "Video Decoder", + 4096: "Audio Tuner", + 4097: "Audio Line", + 4098: "Audio Mic", + 4099: "AES Digital", + 4100: "SPDIF Digital", + 4101: "SCSI Audio", + 4102: "AUX Audio", + 4103: "1394 Audio", + 4104: "USB Audio", + 4105: "Audio Decoder", +} + +VIDEO_STANDARD_NAMES = { + 0x00000001: "NTSC_M", + 0x00000002: "NTSC_M_J", + 0x00000004: "NTSC_433", + 0x00000010: "PAL_B", + 0x00000020: "PAL_D", + 0x00000040: "PAL_G", + 0x00000080: "PAL_H", + 0x00000100: "PAL_I", + 0x00000200: "PAL_M", + 0x00000400: "PAL_N", + 0x00000800: "PAL_60", + 0x00001000: "SECAM_B", + 0x00002000: "SECAM_D", + 0x00004000: "SECAM_G", + 0x00008000: "SECAM_H", + 0x00010000: "SECAM_K", + 0x00020000: "SECAM_K1", + 0x00040000: "SECAM_L", + 0x00080000: "SECAM_L1", + 0x00100000: "PAL_N_COMBO", +} + + +class IMoniker(IUnknown): + _iid_ = GUID("{0000000F-0000-0000-C000-000000000046}") + _methods_ = [ + COMMETHOD([], HRESULT, "GetClassID", (["out"], POINTER(GUID), "pClassID")), + COMMETHOD([], HRESULT, "IsDirty"), + COMMETHOD([], HRESULT, "Load", (["in"], POINTER(IUnknown), "pStm")), + COMMETHOD([], HRESULT, "Save", (["in"], POINTER(IUnknown), "pStm"), (["in"], c_long, "fClearDirty")), + COMMETHOD([], HRESULT, "GetSizeMax", (["out"], POINTER(c_ulong), "pcbSize")), + COMMETHOD([], HRESULT, "BindToObject", (["in"], POINTER(IUnknown), "pbc"), (["in"], POINTER(IUnknown), "pmkToLeft"), (["in"], POINTER(GUID), "riidResult"), (["out"], POINTER(POINTER(IUnknown)), "ppvResult")), + COMMETHOD([], HRESULT, "BindToStorage", (["in"], POINTER(IUnknown), "pbc"), (["in"], POINTER(IUnknown), "pmkToLeft"), (["in"], POINTER(GUID), "riid"), (["out"], POINTER(POINTER(IUnknown)), "ppvObj")), + ] + + +class IEnumMoniker(IUnknown): + _iid_ = GUID("{00000102-0000-0000-C000-000000000046}") + _methods_ = [ + COMMETHOD([], HRESULT, "Next", (["in"], c_ulong, "celt"), (["out"], POINTER(POINTER(IMoniker)), "rgelt"), (["out"], POINTER(c_ulong), "pceltFetched")), + ] + + +class ICreateDevEnum(IUnknown): + _iid_ = GUID("{29840822-5B84-11D0-BD3B-00A0C911CE86}") + _methods_ = [ + COMMETHOD([], HRESULT, "CreateClassEnumerator", (["in"], POINTER(GUID), "clsidDeviceClass"), (["out"], POINTER(POINTER(IEnumMoniker)), "ppEnumMoniker"), (["in"], c_ulong, "dwFlags")), + ] + + +class IPropertyBag(IUnknown): + _iid_ = GUID("{55272A00-42CB-11CE-8135-00AA004BB851}") + _methods_ = [ + COMMETHOD([], HRESULT, "Read", (["in"], comtypes.c_wchar_p, "pszPropName"), (["out"], POINTER(VARIANT), "pVar"), (["in"], POINTER(IUnknown), "pErrorLog")), + ] + + +class IAMAnalogVideoDecoder(IUnknown): + _iid_ = GUID("{C6E13350-30AC-11d0-A18C-00A0C9118956}") + _methods_ = [ + COMMETHOD([], HRESULT, "get_AvailableTVFormats", (["out"], POINTER(c_long), "lAnalogVideoStandard")), + COMMETHOD([], HRESULT, "put_TVFormat", (["in"], c_long, "lAnalogVideoStandard")), + COMMETHOD([], HRESULT, "get_TVFormat", (["out"], POINTER(c_long), "plAnalogVideoStandard")), + ] + + +class IAMCrossbar(IUnknown): + _iid_ = GUID("{C6E13380-30AC-11d0-A18C-00A0C9118956}") + _methods_ = [ + COMMETHOD([], HRESULT, "get_PinCounts", (["out"], POINTER(c_long), "OutputPinCount"), (["out"], POINTER(c_long), "InputPinCount")), + COMMETHOD([], HRESULT, "CanRoute", (["in"], c_long, "OutputPinIndex"), (["in"], c_long, "InputPinIndex")), + COMMETHOD([], HRESULT, "Route", (["in"], c_long, "OutputPinIndex"), (["in"], c_long, "InputPinIndex")), + COMMETHOD([], HRESULT, "get_IsRoutedTo", (["in"], c_long, "OutputPinIndex"), (["out"], POINTER(c_long), "InputPinIndex")), + COMMETHOD([], HRESULT, "get_CrossbarPinInfo", (["in"], c_long, "IsInputPin"), (["in"], c_long, "PinIndex"), (["out"], POINTER(c_long), "PinIndexRelated"), (["out"], POINTER(c_long), "PhysicalType")), + ] + + +def create_dev_enum(): + return comtypes.client.CreateObject(CLSID_SystemDeviceEnum, interface=ICreateDevEnum) + + +def list_filters(category_guid): + dev_enum = create_dev_enum() + enum_moniker = dev_enum.CreateClassEnumerator(byref(category_guid), 0) + if not enum_moniker: + return [] + + result = [] + invalid_count = 0 + max_invalid = 10 + max_total = 100 + total = 0 + + while total < max_total: + total += 1 + monikers = enum_moniker.Next(1) + + if not monikers: + break + + moniker = monikers[0] + if not moniker: + break + + try: + bag_unknown = moniker.BindToStorage(None, None, byref(IPropertyBag._iid_)) + if not bag_unknown: + invalid_count += 1 + if invalid_count >= max_invalid: + break + continue + + bag = bag_unknown.QueryInterface(IPropertyBag) + name = bag.Read("FriendlyName", None) + + if name: + result.append({"name": name, "moniker": moniker}) + + except Exception: + invalid_count += 1 + if invalid_count >= max_invalid: + break + continue + + return result + + +def bind_filter(moniker): + return moniker.BindToObject(None, None, byref(IID_IBaseFilter)) + + +def get_filter_by_name(category_guid, target_name): + for item in list_filters(category_guid): + if item["name"] == target_name: + return bind_filter(item["moniker"]) + raise RuntimeError(f"Filter nicht gefunden: {target_name}") + + +def decode_video_standards(mask): + return [(value, name) for value, name in VIDEO_STANDARD_NAMES.items() if mask & value] + + +def get_decoder(video_filter): + return video_filter.QueryInterface(IAMAnalogVideoDecoder) + + +def get_crossbar(crossbar_filter): + return crossbar_filter.QueryInterface(IAMCrossbar) + + +def inspect_crossbar(crossbar): + output_count, input_count = crossbar.get_PinCounts() + inputs = [] + outputs = [] + + for i in range(input_count): + related, physical_type = crossbar.get_CrossbarPinInfo(1, i) + inputs.append({ + "index": i, + "related": related, + "physical_type": physical_type, + "name": PHYS_CONN_NAMES.get(physical_type, f"Unknown {physical_type}"), + }) + + for o in range(output_count): + related, physical_type = crossbar.get_CrossbarPinInfo(0, o) + outputs.append({ + "index": o, + "related": related, + "physical_type": physical_type, + "name": PHYS_CONN_NAMES.get(physical_type, f"Unknown {physical_type}"), + }) + + return inputs, outputs + + +def find_index_by_name(items, name): + for i, item in enumerate(items): + if item.get("name") == name: + return i + return -1 + + +class VideoGrabberGui(tk.Tk): + def __init__(self, config_path, force_config=False): + super().__init__() + + self.config_path = config_path + self.force_config = force_config + + self.video_devices = [] + self.crossbars = [] + self.standards = [] + self.inputs = [] + self.outputs = [] + + self.title("Video Grabber Konfiguration") + self.geometry("760x560") + self.minsize(700, 500) + + self._build_ui() + + self.after(200, self.startup) + + def _build_ui(self): + main = ttk.Frame(self, padding=12) + main.pack(fill="both", expand=True) + + main.columnconfigure(1, weight=1) + + ttk.Label(main, text="Videogerät").grid(row=0, column=0, sticky="w", pady=4) + self.video_combo = ttk.Combobox(main, state="readonly") + self.video_combo.grid(row=0, column=1, sticky="ew", pady=4) + self.video_combo.bind("<>", lambda e: self.on_video_selected()) + + ttk.Button(main, text="Aktualisieren", command=self.refresh_devices).grid(row=0, column=2, padx=(8, 0), pady=4) + + ttk.Label(main, text="Videostandard").grid(row=1, column=0, sticky="w", pady=4) + self.standard_combo = ttk.Combobox(main, state="readonly") + self.standard_combo.grid(row=1, column=1, sticky="ew", pady=4) + + ttk.Label(main, text="Crossbar").grid(row=2, column=0, sticky="w", pady=4) + self.crossbar_combo = ttk.Combobox(main, state="readonly") + self.crossbar_combo.grid(row=2, column=1, sticky="ew", pady=4) + self.crossbar_combo.bind("<>", lambda e: self.on_crossbar_selected()) + + ttk.Label(main, text="Eingang").grid(row=3, column=0, sticky="w", pady=4) + self.input_combo = ttk.Combobox(main, state="readonly") + self.input_combo.grid(row=3, column=1, sticky="ew", pady=4) + + ttk.Label(main, text="Ausgang").grid(row=4, column=0, sticky="w", pady=4) + self.output_combo = ttk.Combobox(main, state="readonly") + self.output_combo.grid(row=4, column=1, sticky="ew", pady=4) + + buttons = ttk.Frame(main) + buttons.grid(row=5, column=0, columnspan=3, sticky="ew", pady=(12, 8)) + + ttk.Button(buttons, text="Speichern", command=self.save_config).pack(side="left") + ttk.Button(buttons, text="Anwenden", command=self.apply_current_selection).pack(side="left", padx=8) + ttk.Button(buttons, text="Speichern + Anwenden", command=self.save_and_apply).pack(side="left") + ttk.Button(buttons, text="Beenden", command=self.destroy).pack(side="right") + + ttk.Label(main, text="Protokoll").grid(row=6, column=0, columnspan=3, sticky="w", pady=(12, 2)) + self.log_box = ScrolledText(main, height=16) + self.log_box.grid(row=7, column=0, columnspan=3, sticky="nsew") + main.rowconfigure(7, weight=1) + + def log(self, text): + self.log_box.insert("end", text + "\n") + self.log_box.see("end") + self.update_idletasks() + + def startup(self): + try: + self.refresh_devices() + + if os.path.exists(self.config_path): + self.load_config_into_ui() + if not self.force_config: + self.log("Konfiguration gefunden. Wende gespeicherte Einstellungen an.") + self.apply_config_file() + else: + self.log("Keine config.json gefunden. Bitte Einstellungen auswählen und speichern.") + except Exception as e: + messagebox.showerror("Fehler", str(e)) + self.log(f"Fehler: {e}") + + def refresh_devices(self): + self.log("Suche DirectShow-Videogeräte...") + self.video_devices = list_filters(CLSID_VideoInputDeviceCategory) + self.video_combo["values"] = [d["name"] for d in self.video_devices] + + self.crossbars = list_filters(CLSID_AM_KSCATEGORY_CROSSBAR) + self.crossbar_combo["values"] = [c["name"] for c in self.crossbars] + + self.log(f"Videogeräte: {len(self.video_devices)}") + self.log(f"Crossbars: {len(self.crossbars)}") + + if self.video_devices and self.video_combo.current() < 0: + self.video_combo.current(0) + self.on_video_selected() + + def on_video_selected(self): + idx = self.video_combo.current() + if idx < 0: + return + + device = self.video_devices[idx] + self.log(f"Öffne Videogerät: {device['name']}") + + try: + video_filter = bind_filter(device["moniker"]) + decoder = get_decoder(video_filter) + available_mask = decoder.get_AvailableTVFormats() + + self.standards = decode_video_standards(available_mask) + self.standard_combo["values"] = [f"{name} ({value})" for value, name in self.standards] + + default = next((i for i, (_, name) in enumerate(self.standards) if name == "PAL_B"), None) + if default is None: + default = next((i for i, (_, name) in enumerate(self.standards) if name.startswith("PAL")), 0) + + if self.standards: + self.standard_combo.current(default) + + self.log(f"Verfügbare Videostandards: {available_mask}") + + except Exception as e: + self.standards = [] + self.standard_combo["values"] = [] + self.log(f"Videostandards konnten nicht gelesen werden: {e}") + + self.select_likely_crossbar(device["name"]) + + def select_likely_crossbar(self, device_name): + if not self.crossbars: + return + + base_parts = [ + p.lower() + for p in device_name.replace("Video Capture", "").replace("Capture", "").split() + if len(p) > 2 + ] + + best_index = 0 + best_score = -1 + + for i, item in enumerate(self.crossbars): + name = item["name"].lower() + score = sum(1 for part in base_parts if part in name) + if score > best_score: + best_index = i + best_score = score + + self.crossbar_combo.current(best_index) + self.on_crossbar_selected() + + def on_crossbar_selected(self): + idx = self.crossbar_combo.current() + if idx < 0: + return + + item = self.crossbars[idx] + self.log(f"Öffne Crossbar: {item['name']}") + + try: + crossbar_filter = bind_filter(item["moniker"]) + crossbar = get_crossbar(crossbar_filter) + self.inputs, self.outputs = inspect_crossbar(crossbar) + + self.input_combo["values"] = [ + f"Input {i['index']}: {i['name']} ({i['physical_type']})" + for i in self.inputs + ] + self.output_combo["values"] = [ + f"Output {o['index']}: {o['name']} ({o['physical_type']})" + for o in self.outputs + ] + + input_default = next((i for i, item in enumerate(self.inputs) if item["physical_type"] == 3), 0) + output_default = next((i for i, item in enumerate(self.outputs) if item["physical_type"] == 12), 0) + + if self.inputs: + self.input_combo.current(input_default) + if self.outputs: + self.output_combo.current(output_default) + + except Exception as e: + self.inputs = [] + self.outputs = [] + self.input_combo["values"] = [] + self.output_combo["values"] = [] + self.log(f"Crossbar konnte nicht gelesen werden: {e}") + + def build_config_from_ui(self): + video_idx = self.video_combo.current() + standard_idx = self.standard_combo.current() + + if video_idx < 0: + raise RuntimeError("Kein Videogerät ausgewählt.") + if standard_idx < 0: + raise RuntimeError("Kein Videostandard ausgewählt.") + + device_name = self.video_devices[video_idx]["name"] + standard_value, standard_name = self.standards[standard_idx] + + cfg = { + "device_name": device_name, + "video_standard": { + "name": standard_name, + "value": standard_value, + }, + "crossbar_name": None, + "input": None, + "output": None, + } + + crossbar_idx = self.crossbar_combo.current() + input_idx = self.input_combo.current() + output_idx = self.output_combo.current() + + if crossbar_idx >= 0 and input_idx >= 0 and output_idx >= 0: + cfg["crossbar_name"] = self.crossbars[crossbar_idx]["name"] + cfg["input"] = self.inputs[input_idx] + cfg["output"] = self.outputs[output_idx] + + return cfg + + def save_config(self): + try: + cfg = self.build_config_from_ui() + ensure_config_dir() + with open(self.config_path, "w", encoding="utf-8") as f: + json.dump(cfg, f, indent=4, ensure_ascii=False) + self.log(f"Konfiguration gespeichert: {self.config_path}") + messagebox.showinfo("Gespeichert", f"Konfiguration gespeichert:\n{self.config_path}") + except Exception as e: + messagebox.showerror("Fehler", str(e)) + self.log(f"Fehler beim Speichern: {e}") + + def save_and_apply(self): + self.save_config() + self.apply_config_file() + + def apply_current_selection(self): + try: + cfg = self.build_config_from_ui() + self.apply_config(cfg) + except Exception as e: + messagebox.showerror("Fehler", str(e)) + self.log(f"Fehler beim Anwenden: {e}") + + def apply_config_file(self): + with open(self.config_path, "r", encoding="utf-8") as f: + cfg = json.load(f) + self.apply_config(cfg) + + def apply_config(self, cfg): + device_name = cfg["device_name"] + standard_name = cfg["video_standard"]["name"] + standard_value = int(cfg["video_standard"]["value"]) + + self.log(f"Öffne Videogerät: {device_name}") + video_filter = get_filter_by_name(CLSID_VideoInputDeviceCategory, device_name) + decoder = get_decoder(video_filter) + + try: + before = decoder.get_TVFormat() + self.log(f"Vorheriger Videostandard: {before}") + except Exception: + pass + + decoder.put_TVFormat(standard_value) + self.log(f"Videostandard gesetzt: {standard_name} ({standard_value})") + + try: + after = decoder.get_TVFormat() + self.log(f"Aktueller Videostandard: {after}") + except Exception: + pass + + crossbar_name = cfg.get("crossbar_name") + input_cfg = cfg.get("input") + output_cfg = cfg.get("output") + + if not crossbar_name or not input_cfg or not output_cfg: + self.log("Keine Crossbar-/Eingangskonfiguration vorhanden.") + return + + self.log(f"Öffne Crossbar: {crossbar_name}") + crossbar_filter = get_filter_by_name(CLSID_AM_KSCATEGORY_CROSSBAR, crossbar_name) + crossbar = get_crossbar(crossbar_filter) + + output_index = int(output_cfg["index"]) + input_index = int(input_cfg["index"]) + + crossbar.CanRoute(output_index, input_index) + crossbar.Route(output_index, input_index) + + self.log(f"Eingang gesetzt: Output {output_index} -> Input {input_index} ({input_cfg.get('name', 'unbekannt')})") + + def load_config_into_ui(self): + try: + with open(self.config_path, "r", encoding="utf-8") as f: + cfg = json.load(f) + + device_idx = find_index_by_name(self.video_devices, cfg.get("device_name")) + if device_idx >= 0: + self.video_combo.current(device_idx) + self.on_video_selected() + + standard_cfg = cfg.get("video_standard", {}) + standard_value = int(standard_cfg.get("value", -1)) + for i, (value, _) in enumerate(self.standards): + if value == standard_value: + self.standard_combo.current(i) + break + + crossbar_idx = find_index_by_name(self.crossbars, cfg.get("crossbar_name")) + if crossbar_idx >= 0: + self.crossbar_combo.current(crossbar_idx) + self.on_crossbar_selected() + + input_cfg = cfg.get("input") or {} + output_cfg = cfg.get("output") or {} + + for i, item in enumerate(self.inputs): + if item["index"] == input_cfg.get("index"): + self.input_combo.current(i) + break + + for i, item in enumerate(self.outputs): + if item["index"] == output_cfg.get("index"): + self.output_combo.current(i) + break + + self.log("Gespeicherte Konfiguration geladen.") + + except Exception as e: + self.log(f"Konfiguration konnte nicht geladen werden: {e}") + + +def apply_config_headless(config_path): + with open(config_path, "r", encoding="utf-8") as f: + cfg = json.load(f) + + device_name = cfg["device_name"] + standard_name = cfg["video_standard"]["name"] + standard_value = int(cfg["video_standard"]["value"]) + + print(f"Öffne Videogerät: {device_name}") + video_filter = get_filter_by_name(CLSID_VideoInputDeviceCategory, device_name) + decoder = get_decoder(video_filter) + + try: + before = decoder.get_TVFormat() + print(f"Vorheriger Videostandard: {before}") + except Exception: + pass + + decoder.put_TVFormat(standard_value) + print(f"Videostandard gesetzt: {standard_name} ({standard_value})") + + try: + after = decoder.get_TVFormat() + print(f"Aktueller Videostandard: {after}") + except Exception: + pass + + crossbar_name = cfg.get("crossbar_name") + input_cfg = cfg.get("input") + output_cfg = cfg.get("output") + + if not crossbar_name or not input_cfg or not output_cfg: + print("Keine Crossbar-/Eingangskonfiguration vorhanden.") + return + + print(f"Öffne Crossbar: {crossbar_name}") + crossbar_filter = get_filter_by_name(CLSID_AM_KSCATEGORY_CROSSBAR, crossbar_name) + crossbar = get_crossbar(crossbar_filter) + + output_index = int(output_cfg["index"]) + input_index = int(input_cfg["index"]) + + crossbar.CanRoute(output_index, input_index) + crossbar.Route(output_index, input_index) + + print(f"Eingang gesetzt: Output {output_index} -> Input {input_index} ({input_cfg.get('name', 'unbekannt')})") + + +def main(): + parser = argparse.ArgumentParser(description="GUI für DirectShow Video Grabber Konfiguration") + parser.add_argument("mode", nargs="?", default="", help="/config öffnet die Konfigurationsoberfläche") + parser.add_argument("--config-file", default=CONFIG_FILE, help="Pfad zur config.json") + args = parser.parse_args() + + force_config = args.mode.lower() in ("/config", "config", "--config") + ensure_config_dir() + config_exists = os.path.exists(args.config_file) + + # Normalstart: + # - config.json vorhanden und kein /config: nur anwenden und beenden + # - keine config.json oder /config: GUI öffnen + if config_exists and not force_config: + try: + apply_config_headless(args.config_file) + return + except Exception as e: + print(f"Fehler beim Anwenden der Konfiguration: {e}") + print("Starte Konfigurationsoberfläche.") + app = VideoGrabberGui(args.config_file, force_config=True) + app.mainloop() + return + + app = VideoGrabberGui(args.config_file, force_config=True) + app.mainloop() + + +if __name__ == "__main__": + main()