diff --git a/app/settings.py b/app/settings.py index 4f0510a2..649a14b7 100644 --- a/app/settings.py +++ b/app/settings.py @@ -28,6 +28,18 @@ class Defaults(Enum): FAV_CLICK_MODE = 0 PROFILE_FOLDER_DEFAULT = False RECORDS_PATH = DATA_PATH + "records/" + ACTIVATE_TRANSCODING = False + ACTIVE_TRANSCODING_PRESET = "720p TV/device" + + +def get_settings(): + os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True) + + if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0: + write_settings(get_default_settings()) + + with open(CONFIG_FILE, "r") as config_file: + return json.load(config_file) def get_default_settings(profile_name="default"): @@ -51,6 +63,18 @@ def get_default_settings(profile_name="default"): } +def get_default_transcoding_presets(): + return {"720p TV/device": {"vcodec": "h264", "vb": "1500", "width": "1280", "height": "720", "acodec": "mp3", + "ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"}, + "1080p TV/device": {"vcodec": "h264", "vb": "3500", "width": "1920", "height": "1080", "acodec": "mp3", + "ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"}} + + +def write_settings(config): + with open(CONFIG_FILE, "w") as config_file: + json.dump(config, config_file, indent=" ") + + def set_local_paths(settings, profile_name, data_path=DATA_PATH, use_profile_folder=False): settings["data_local_path"] = "{}{}/".format(data_path, profile_name) if use_profile_folder: @@ -404,7 +428,33 @@ class Settings: def records_path(self, value): self._settings["records_path"] = value - # ***** Program settings ***** + # ******** Streaming ********* # + + @property + def activate_transcoding(self): + return self._settings.get("activate_transcoding", Defaults.ACTIVATE_TRANSCODING.value) + + @activate_transcoding.setter + def activate_transcoding(self, value): + self._settings["activate_transcoding"] = value + + @property + def active_preset(self): + return self._settings.get("active_preset", Defaults.ACTIVE_TRANSCODING_PRESET.value) + + @active_preset.setter + def active_preset(self, value): + self._settings["active_preset"] = value + + @property + def transcoding_presets(self): + return self._settings.get("transcoding_presets", get_default_transcoding_presets()) + + @transcoding_presets.setter + def transcoding_presets(self, value): + self._settings["transcoding_presets"] = value + + # ***** Program settings ***** # @property def backup_before_save(self): @@ -487,20 +537,5 @@ class Settings: self._settings["fav_click_mode"] = value -def get_settings(): - os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True) - - if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0: - write_settings(get_default_settings()) - - with open(CONFIG_FILE, "r") as config_file: - return json.load(config_file) - - -def write_settings(config): - with open(CONFIG_FILE, "w") as config_file: - json.dump(config, config_file, indent=" ") - - if __name__ == "__main__": pass diff --git a/app/tools/media.py b/app/tools/media.py index 4ba89ec1..45b69723 100644 --- a/app/tools/media.py +++ b/app/tools/media.py @@ -108,9 +108,10 @@ class Player: class Recorder: __VLC_REC_INSTANCE = None - _CMD = "sout=#std{{access=file,mux=ts,dst={}{}_{}.ts}}" + _CMD = "sout=#std{{access=file,mux=ts,dst={}.ts}}" + _TR_CMD = "sout=#transcode{{{}}}:file{{mux=mp4,dst={}.mp4}}" - def __init__(self): + def __init__(self, settings): try: from app.tools import vlc from app.tools.vlc import EventType @@ -118,24 +119,28 @@ class Recorder: log("{}: Load library error: {}".format(__class__.__name__, e)) raise ImportError else: + self._settings = settings self._is_record = False args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib") self._recorder = vlc.Instance(args).media_player_new() @classmethod - def get_instance(cls): + def get_instance(cls, settings): if not cls.__VLC_REC_INSTANCE: - cls.__VLC_REC_INSTANCE = Recorder() + cls.__VLC_REC_INSTANCE = Recorder(settings) return cls.__VLC_REC_INSTANCE @run_task - def record(self, url, path, name): + def record(self, url, name): if self._recorder: self._recorder.stop() + path = self._settings.records_path os.makedirs(os.path.dirname(path), exist_ok=True) d_now = datetime.now().strftime(_DATE_FORMAT) - media = self._recorder.get_instance().media_new(url, self._CMD.format(path, name, d_now)) + path = "{}{}_{}".format(path, name.replace(" ", "_"), d_now.replace(" ", "_")) + cmd = self.get_transcoding_cmd(path) if self._settings.activate_transcoding else self._CMD.format(path) + media = self._recorder.get_instance().media_new(url, cmd) media.get_mrl() self._recorder.set_media(media) @@ -160,6 +165,11 @@ class Recorder: self._is_record = False log("Recording stopped. Releasing...") + def get_transcoding_cmd(self, path): + presets = self._settings.transcoding_presets + prs = presets.get(self._settings.active_preset) + return self._TR_CMD.format(",".join("{}={}".format(k, v) for k, v in prs.items()), path) + if __name__ == "__main__": pass diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index c4efea04..02cca976 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -1842,7 +1842,7 @@ class Application(Gtk.Application): if not self._recorder: try: - self._recorder = Recorder.get_instance() + self._recorder = Recorder.get_instance(self._settings) except (ImportError, NameError, AttributeError): self.show_error_dialog("No VLC is found. Check that it is installed!") return @@ -1858,7 +1858,7 @@ class Application(Gtk.Application): if m3u: url = [s for s in m3u.split("\n") if not s.startswith("#")] if url: - self._recorder.record(url[0], self._settings.records_path, self._service_name_label.get_text()) + self._recorder.record(url[0], self._service_name_label.get_text()) GLib.timeout_add_seconds(1, self.update_record_button, priority=GLib.PRIORITY_LOW) def update_record_button(self): diff --git a/app/ui/settings_dialog.glade b/app/ui/settings_dialog.glade index bcb4e402..8c1b8e32 100644 --- a/app/ui/settings_dialog.glade +++ b/app/ui/settings_dialog.glade @@ -3,7 +3,7 @@ The MIT License (MIT) -Copyright (c) 2018 Dmitriy Yefremov +Copyright (c) 2018-2020 Dmitriy Yefremov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -31,7 +31,7 @@ Author: Dmitriy Yefremov - + @@ -206,6 +206,25 @@ Author: Dmitriy Yefremov 4 + + + False + True + True + Apply + + + + True + False + gtk-apply + + + + + 5 + + True @@ -773,6 +792,7 @@ Author: Dmitriy Yefremov 6 21 network-workgroup-symbolic + 2 @@ -856,6 +876,7 @@ Author: Dmitriy Yefremov 6 80 network-workgroup-symbolic + 2 @@ -911,6 +932,7 @@ Author: Dmitriy Yefremov 6 23 network-workgroup-symbolic + 2 @@ -1315,7 +1337,7 @@ Author: Dmitriy Yefremov True False - Make profile folder as default for the additional data. + Make profile folder as default for the additional data False @@ -1465,6 +1487,456 @@ Author: Dmitriy Yefremov 1 + + + True + False + vertical + 5 + + + True + False + 5 + 5 + 5 + 0.019999999552965164 + in + + + True + False + 5 + 5 + 5 + vertical + 5 + + + True + False + 5 + + + True + False + Activate transcoding + + + False + True + 0 + + + + + True + True + + + False + True + end + 1 + + + + + True + True + 0 + + + + + True + False + False + vertical + 5 + + + True + False + 5 + + + True + False + start + Presets: + + + True + True + 0 + + + + + True + False + 720p TV/device + + 720p TV/device + 1080p TV/device + + + + + True + True + 1 + + + + + False + True + 0 + + + + + True + False + Edit + end + 5 + + + True + False + gtk-edit + + + False + True + 0 + + + + + True + True + + + False + True + end + 1 + + + + + True + True + 1 + + + + + True + False + False + 0.5 + in + + + True + False + center + 5 + 5 + 5 + 5 + 2 + 5 + + + True + True + number + + + + 0 + 1 + + + + + True + False + Bitrate (kb/s): + + + 0 + 0 + + + + + True + True + + + number + + + + 1 + 1 + + + + + True + False + Width (px): + + + 1 + 0 + + + + + True + False + Codec: + + + 3 + 0 + + + + + h264 + True + True + False + center + True + True + + + 3 + 1 + + + + + True + True + number + + + + 2 + 1 + + + + + True + False + Height (px): + + + 2 + 0 + + + + + + + True + False + Video options: + + + + + False + True + 2 + + + + + True + False + False + 0.5 + in + + + True + False + center + 5 + 5 + 5 + 5 + 2 + 5 + + + True + True + number + + + + 0 + 1 + + + + + True + False + Bitrate (kb/s): + + + 0 + 0 + + + + + True + False + Channels: + + + 1 + 0 + + + + + True + False + Codec: + + + 3 + 0 + + + + + mp3 + True + True + False + center + True + True + + + 3 + 1 + + + + + True + False + Sample rate (Hz): + + + 2 + 0 + + + + + 75 + True + False + 2 + + 1 + 2 + + + + 1 + 1 + + + + + True + False + 44100 + + 8000 + 11025 + 22050 + 44100 + 48000 + + + + 2 + 1 + + + + + + + True + False + Audio options: + + + + + False + True + 3 + + + + + True + True + 1 + + + + + + + True + False + Record to disk: + + + + + False + True + 0 + + + + + + + + streaming + Streaming + 2 + + True @@ -1818,7 +2290,7 @@ Author: Dmitriy Yefremov program Program - 2 + 3 @@ -2148,7 +2620,7 @@ Author: Dmitriy Yefremov extra Extra - 3 + 4 diff --git a/app/ui/settings_dialog.py b/app/ui/settings_dialog.py index 85014083..71370218 100644 --- a/app/ui/settings_dialog.py +++ b/app/ui/settings_dialog.py @@ -1,4 +1,5 @@ import os +import re from enum import Enum from app.commons import run_task, run_idle @@ -20,6 +21,8 @@ class Property(Enum): class SettingsDialog: + _DIGIT_ENTRY_NAME = "digit-entry" + _DIGIT_PATTERN = re.compile("(?:^[\\s]*$|\\D)") def __init__(self, transient, settings: Settings): handlers = {"on_field_icon_press": self.on_field_icon_press, @@ -33,7 +36,6 @@ class SettingsDialog: "on_set_color_switch": self.on_set_color_switch, "on_http_mode_switch": self.on_http_mode_switch, "on_yt_dl_switch": self.on_yt_dl_switch, - "on_send_to_switch": self.on_send_to_switch, "on_default_path_mode_switch": self.on_default_path_mode_switch, "on_default_data_path_changed": self.on_default_data_path_changed, "on_profile_add": self.on_profile_add, @@ -49,6 +51,9 @@ class SettingsDialog: "on_network_settings_visible": self.on_network_settings_visible, "on_http_use_ssl_toggled": self.on_http_use_ssl_toggled, "on_click_mode_togged": self.on_click_mode_togged, + "on_transcoding_preset_changed": self.on_transcoding_preset_changed, + "on_apply_presets": self.on_apply_presets, + "on_digit_entry_changed": self.on_digit_entry_changed, "on_view_popup_menu": self.on_view_popup_menu} builder = Gtk.Builder() @@ -92,6 +97,23 @@ class SettingsDialog: self._enigma_radio_button = builder.get_object("enigma_radio_button") self._neutrino_radio_button = builder.get_object("neutrino_radio_button") self._support_ver5_switch = builder.get_object("support_ver5_switch") + # Streaming + header_separator = builder.get_object("header_separator") + self._apply_presets_button = builder.get_object("apply_presets_button") + self._transcoding_switch = builder.get_object("transcoding_switch") + self._edit_preset_switch = builder.get_object("edit_preset_switch") + self._presets_combo_box = builder.get_object("presets_combo_box") + self._video_bitrate_field = builder.get_object("video_bitrate_field") + self._video_width_field = builder.get_object("video_width_field") + self._video_height_field = builder.get_object("video_height_field") + self._audio_bitrate_field = builder.get_object("audio_bitrate_field") + self._audio_channels_combo_box = builder.get_object("audio_channels_combo_box") + self._audio_sample_rate_combo_box = builder.get_object("audio_sample_rate_combo_box") + self._apply_presets_button.bind_property("visible", header_separator, "visible") + self._transcoding_switch.bind_property("active", builder.get_object("record_box"), "sensitive") + self._edit_preset_switch.bind_property("active", self._apply_presets_button, "sensitive") + self._edit_preset_switch.bind_property("active", builder.get_object("video_options_frame"), "sensitive") + self._edit_preset_switch.bind_property("active", builder.get_object("audio_options_frame"), "sensitive") # Program self._before_save_switch = builder.get_object("before_save_switch") self._before_downloading_switch = builder.get_object("before_downloading_switch") @@ -121,10 +143,18 @@ class SettingsDialog: self._profile_add_button = builder.get_object("profile_add_button") self._profile_remove_button = builder.get_object("profile_remove_button") self._apply_profile_button = builder.get_object("apply_profile_button") - self._apply_profile_button.bind_property("visible", builder.get_object("header_separator"), "visible") + self._apply_profile_button.bind_property("visible", header_separator, "visible") self._apply_profile_button.bind_property("visible", builder.get_object("reset_button"), "visible") # Language self._lang_combo_box = builder.get_object("lang_combo_box") + # Style + self._style_provider = Gtk.CssProvider() + self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css") + self._digit_elems = (self._port_field, self._http_port_field, self._telnet_port_field, self._video_width_field, + self._video_bitrate_field, self._video_height_field, self._audio_bitrate_field) + for el in self._digit_elems: + el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider, + Gtk.STYLE_PROVIDER_PRIORITY_USER) # Settings self._ext_settings = settings self._settings = Settings(settings.settings) @@ -218,6 +248,9 @@ class SettingsDialog: self.set_fav_click_mode(self._settings.fav_click_mode) self._load_on_startup_switch.set_active(self._settings.load_last_config) self._default_data_paths_switch.set_active(self._settings.profile_folder_is_default) + self._transcoding_switch.set_active(self._settings.activate_transcoding) + self._presets_combo_box.set_active_id(self._settings.active_preset) + self.on_transcoding_preset_changed(self._presets_combo_box) if self._s_type is SettingsType.ENIGMA_2: self._support_ver5_switch.set_active(self._settings.v5_support) @@ -238,6 +271,10 @@ class SettingsDialog: self._neutrino_radio_button.activate() def on_apply_profile_settings(self, item): + if not self.is_data_correct(self._digit_elems): + show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!") + return + self._s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP self._settings.setting_type = self._s_type self._settings.host = self._host_field.get_text() @@ -273,6 +310,8 @@ class SettingsDialog: self._ext_settings.profile_folder_is_default = self._default_data_paths_switch.get_active() self._ext_settings.default_data_path = self._default_data_dir_field.get_text() self._ext_settings.records_path = self._record_data_dir_field.get_text() + self._ext_settings.activate_transcoding = self._transcoding_switch.get_active() + self._ext_settings.active_preset = self._presets_combo_box.get_active_id() if self._s_type is SettingsType.ENIGMA_2: self._ext_settings.use_colors = self._set_color_switch.get_active() @@ -361,9 +400,6 @@ class SettingsDialog: def on_yt_dl_switch(self, switch, state): self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING) - def on_send_to_switch(self, switch, state): - self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING) - def on_default_path_mode_switch(self, switch, state): self._settings.profile_folder_is_default = state @@ -462,7 +498,9 @@ class SettingsDialog: self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING) def on_main_settings_visible(self, stack, param): - self._apply_profile_button.set_visible(stack.get_visible_child_name() == "profiles") + name = stack.get_visible_child_name() + self._apply_profile_button.set_visible(name == "profiles") + self._apply_presets_button.set_visible(name == "streaming") def on_network_settings_visible(self, stack, param): self._http_use_ssl_check_button.set_visible(Property(stack.get_visible_child_name()) is Property.HTTP) @@ -505,6 +543,44 @@ class SettingsDialog: return FavClickMode.DISABLED + def on_transcoding_preset_changed(self, button): + presets = self._settings.transcoding_presets + prs = presets.get(button.get_active_id()) + self._video_bitrate_field.set_text(prs.get("vb", "0")) + self._video_width_field.set_text(prs.get("width", "0")) + self._video_height_field.set_text(prs.get("height", "0")) + self._audio_bitrate_field.set_text(prs.get("ab", "0")) + self._audio_channels_combo_box.set_active_id(prs.get("channels", "2")) + self._audio_sample_rate_combo_box.set_active_id(prs.get("samplerate", "44100")) + + def on_apply_presets(self, item): + if not self.is_data_correct(self._digit_elems): + show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!") + return + + if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL: + return + + presets = self._settings.transcoding_presets + prs = presets.get(self._presets_combo_box.get_active_id()) + prs["vb"] = self._video_bitrate_field.get_text() + prs["width"] = self._video_width_field.get_text() + prs["height"] = self._video_height_field.get_text() + prs["ab"] = self._audio_bitrate_field.get_text() + prs["channels"] = self._audio_channels_combo_box.get_active_id() + prs["samplerate"] = self._audio_sample_rate_combo_box.get_active_id() + self._ext_settings.transcoding_presets = presets + self._edit_preset_switch.set_active(False) + + def on_digit_entry_changed(self, entry): + if self._DIGIT_PATTERN.search(entry.get_text()): + entry.set_name(self._DIGIT_ENTRY_NAME) + else: + entry.set_name("GtkEntry") + + def is_data_correct(self, elems): + return not any(elem.get_name() == self._DIGIT_ENTRY_NAME for elem in elems) + def on_view_popup_menu(self, menu, event): if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY: menu.popup(None, None, None, None, event.button, event.time)