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
-
+
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)