nixos/epgstation: changes for EPGStation v2
This commit is contained in:
parent
5f388fdc23
commit
5e7be6b480
2 changed files with 311 additions and 278 deletions
|
@ -1,30 +1,40 @@
|
||||||
{ config, lib, options, pkgs, ... }:
|
{ config, lib, options, pkgs, ... }:
|
||||||
|
|
||||||
with lib;
|
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.services.epgstation;
|
cfg = config.services.epgstation;
|
||||||
opt = options.services.epgstation;
|
opt = options.services.epgstation;
|
||||||
|
|
||||||
|
description = "EPGStation: DVR system for Mirakurun-managed TV tuners";
|
||||||
|
|
||||||
username = config.users.users.epgstation.name;
|
username = config.users.users.epgstation.name;
|
||||||
groupname = config.users.users.epgstation.group;
|
groupname = config.users.users.epgstation.group;
|
||||||
|
mirakurun = {
|
||||||
|
sock = config.services.mirakurun.unixSocket;
|
||||||
|
option = options.services.mirakurun.unixSocket;
|
||||||
|
};
|
||||||
|
|
||||||
settingsFmt = pkgs.formats.json {};
|
yaml = pkgs.formats.yaml { };
|
||||||
settingsTemplate = settingsFmt.generate "config.json" cfg.settings;
|
settingsTemplate = yaml.generate "config.yml" cfg.settings;
|
||||||
preStartScript = pkgs.writeScript "epgstation-prestart" ''
|
preStartScript = pkgs.writeScript "epgstation-prestart" ''
|
||||||
#!${pkgs.runtimeShell}
|
#!${pkgs.runtimeShell}
|
||||||
|
|
||||||
PASSWORD="$(head -n1 "${cfg.basicAuth.passwordFile}")"
|
DB_PASSWORD_FILE=${lib.escapeShellArg cfg.database.passwordFile}
|
||||||
DB_PASSWORD="$(head -n1 "${cfg.database.passwordFile}")"
|
|
||||||
|
if [[ ! -f "$DB_PASSWORD_FILE" ]]; then
|
||||||
|
printf "[FATAL] File containing the DB password was not found in '%s'. Double check the NixOS option '%s'." \
|
||||||
|
"$DB_PASSWORD_FILE" ${lib.escapeShellArg opt.database.passwordFile} >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DB_PASSWORD="$(head -n1 ${lib.escapeShellArg cfg.database.passwordFile})"
|
||||||
|
|
||||||
# setup configuration
|
# setup configuration
|
||||||
touch /etc/epgstation/config.json
|
touch /etc/epgstation/config.yml
|
||||||
chmod 640 /etc/epgstation/config.json
|
chmod 640 /etc/epgstation/config.yml
|
||||||
sed \
|
sed \
|
||||||
-e "s,@password@,$PASSWORD,g" \
|
|
||||||
-e "s,@dbPassword@,$DB_PASSWORD,g" \
|
-e "s,@dbPassword@,$DB_PASSWORD,g" \
|
||||||
${settingsTemplate} > /etc/epgstation/config.json
|
${settingsTemplate} > /etc/epgstation/config.yml
|
||||||
chown "${username}:${groupname}" /etc/epgstation/config.json
|
chown "${username}:${groupname}" /etc/epgstation/config.yml
|
||||||
|
|
||||||
# NOTE: Use password authentication, since mysqljs does not yet support auth_socket
|
# NOTE: Use password authentication, since mysqljs does not yet support auth_socket
|
||||||
if [ ! -e /var/lib/epgstation/db-created ]; then
|
if [ ! -e /var/lib/epgstation/db-created ]; then
|
||||||
|
@ -35,7 +45,7 @@ let
|
||||||
'';
|
'';
|
||||||
|
|
||||||
streamingConfig = lib.importJSON ./streaming.json;
|
streamingConfig = lib.importJSON ./streaming.json;
|
||||||
logConfig = {
|
logConfig = yaml.generate "logConfig.yml" {
|
||||||
appenders.stdout.type = "stdout";
|
appenders.stdout.type = "stdout";
|
||||||
categories = {
|
categories = {
|
||||||
default = { appenders = [ "stdout" ]; level = "info"; };
|
default = { appenders = [ "stdout" ]; level = "info"; };
|
||||||
|
@ -45,53 +55,51 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultPassword = "INSECURE_GO_CHECK_CONFIGURATION_NIX\n";
|
# Deprecate top level options that are redundant.
|
||||||
|
deprecateTopLevelOption = config:
|
||||||
|
lib.mkRenamedOptionModule
|
||||||
|
([ "services" "epgstation" ] ++ config)
|
||||||
|
([ "services" "epgstation" "settings" ] ++ config);
|
||||||
|
|
||||||
|
removeOption = config: instruction:
|
||||||
|
lib.mkRemovedOptionModule
|
||||||
|
([ "services" "epgstation" ] ++ config)
|
||||||
|
instruction;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.services.epgstation = {
|
meta.maintainers = with lib.maintainers; [ midchildan ];
|
||||||
enable = mkEnableOption "EPGStation: DTV Software in Japan";
|
|
||||||
|
|
||||||
usePreconfiguredStreaming = mkOption {
|
imports = [
|
||||||
type = types.bool;
|
(deprecateTopLevelOption [ "port" ])
|
||||||
|
(deprecateTopLevelOption [ "socketioPort" ])
|
||||||
|
(deprecateTopLevelOption [ "clientSocketioPort" ])
|
||||||
|
(removeOption [ "basicAuth" ]
|
||||||
|
"Use a TLS-terminated reverse proxy with authentication instead.")
|
||||||
|
];
|
||||||
|
|
||||||
|
options.services.epgstation = {
|
||||||
|
enable = lib.mkEnableOption description;
|
||||||
|
|
||||||
|
package = lib.mkOption {
|
||||||
|
default = pkgs.epgstation;
|
||||||
|
type = lib.types.package;
|
||||||
|
defaultText = lib.literalExpression "pkgs.epgstation";
|
||||||
|
description = "epgstation package to use";
|
||||||
|
};
|
||||||
|
|
||||||
|
usePreconfiguredStreaming = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = ''
|
description = ''
|
||||||
Use preconfigured default streaming options.
|
Use preconfigured default streaming options.
|
||||||
|
|
||||||
Upstream defaults:
|
Upstream defaults:
|
||||||
<link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/config/config.sample.json"/>
|
<link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/config/config.yml.template"/>
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
port = mkOption {
|
openFirewall = lib.mkOption {
|
||||||
type = types.port;
|
type = lib.types.bool;
|
||||||
default = 20772;
|
|
||||||
description = ''
|
|
||||||
HTTP port for EPGStation to listen on.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
socketioPort = mkOption {
|
|
||||||
type = types.port;
|
|
||||||
default = cfg.port + 1;
|
|
||||||
defaultText = literalExpression "config.${opt.port} + 1";
|
|
||||||
description = ''
|
|
||||||
Socket.io port for EPGStation to listen on.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
clientSocketioPort = mkOption {
|
|
||||||
type = types.port;
|
|
||||||
default = cfg.socketioPort;
|
|
||||||
defaultText = literalExpression "config.${opt.socketioPort}";
|
|
||||||
description = ''
|
|
||||||
Socket.io port that the web client is going to connect to. This may be
|
|
||||||
different from <option>socketioPort</option> if EPGStation is hidden
|
|
||||||
behind a reverse proxy.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
openFirewall = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = false;
|
default = false;
|
||||||
description = ''
|
description = ''
|
||||||
Open ports in the firewall for the EPGStation web interface.
|
Open ports in the firewall for the EPGStation web interface.
|
||||||
|
@ -106,50 +114,17 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
basicAuth = {
|
database = {
|
||||||
user = mkOption {
|
name = lib.mkOption {
|
||||||
type = with types; nullOr str;
|
type = lib.types.str;
|
||||||
default = null;
|
|
||||||
example = "epgstation";
|
|
||||||
description = ''
|
|
||||||
Basic auth username for EPGStation. If <literal>null</literal>, basic
|
|
||||||
auth will be disabled.
|
|
||||||
|
|
||||||
<warning>
|
|
||||||
<para>
|
|
||||||
Basic authentication has known weaknesses, the most critical being
|
|
||||||
that it sends passwords over the network in clear text. Use this
|
|
||||||
feature to control access to EPGStation within your family and
|
|
||||||
friends, but don't rely on it for security.
|
|
||||||
</para>
|
|
||||||
</warning>
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
passwordFile = mkOption {
|
|
||||||
type = types.path;
|
|
||||||
default = pkgs.writeText "epgstation-password" defaultPassword;
|
|
||||||
defaultText = literalDocBook ''a file containing <literal>${defaultPassword}</literal>'';
|
|
||||||
example = "/run/keys/epgstation-password";
|
|
||||||
description = ''
|
|
||||||
A file containing the password for <option>basicAuth.user</option>.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
database = {
|
|
||||||
name = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "epgstation";
|
default = "epgstation";
|
||||||
description = ''
|
description = ''
|
||||||
Name of the MySQL database that holds EPGStation's data.
|
Name of the MySQL database that holds EPGStation's data.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
passwordFile = mkOption {
|
passwordFile = lib.mkOption {
|
||||||
type = types.path;
|
type = lib.types.path;
|
||||||
default = pkgs.writeText "epgstation-db-password" defaultPassword;
|
|
||||||
defaultText = literalDocBook ''a file containing <literal>${defaultPassword}</literal>'';
|
|
||||||
example = "/run/keys/epgstation-db-password";
|
example = "/run/keys/epgstation-db-password";
|
||||||
description = ''
|
description = ''
|
||||||
A file containing the password for the database named
|
A file containing the password for the database named
|
||||||
|
@ -158,69 +133,106 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
settings = mkOption {
|
# The defaults for some options come from the upstream template
|
||||||
|
# configuration, which is the one that users would get if they follow the
|
||||||
|
# upstream instructions. This is, in some cases, different from the
|
||||||
|
# application defaults. Some options like encodeProcessNum and
|
||||||
|
# concurrentEncodeNum doesn't have an optimal default value that works for
|
||||||
|
# all hardware setups and/or performance requirements. For those kind of
|
||||||
|
# options, the application default wouldn't always result in the expected
|
||||||
|
# out-of-the-box behavior because it's the responsibility of the user to
|
||||||
|
# configure them according to their needs. In these cases, the value in the
|
||||||
|
# upstream template configuration should serve as a "good enough" default.
|
||||||
|
settings = lib.mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Options to add to config.json.
|
Options to add to config.yml.
|
||||||
|
|
||||||
Documentation:
|
Documentation:
|
||||||
<link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/doc/conf-manual.md"/>
|
<link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/doc/conf-manual.md"/>
|
||||||
'';
|
'';
|
||||||
|
|
||||||
default = {};
|
default = { };
|
||||||
example = {
|
example = {
|
||||||
recPriority = 20;
|
recPriority = 20;
|
||||||
conflictPriority = 10;
|
conflictPriority = 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
type = types.submodule {
|
type = lib.types.submodule {
|
||||||
freeformType = settingsFmt.type;
|
freeformType = yaml.type;
|
||||||
|
|
||||||
options.readOnlyOnce = mkOption {
|
options.port = lib.mkOption {
|
||||||
type = types.bool;
|
type = lib.types.port;
|
||||||
default = false;
|
default = 20772;
|
||||||
description = "Don't reload configuration files at runtime.";
|
description = ''
|
||||||
|
HTTP port for EPGStation to listen on.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
options.mirakurunPath = mkOption (let
|
options.socketioPort = lib.mkOption {
|
||||||
sockPath = config.services.mirakurun.unixSocket;
|
type = lib.types.port;
|
||||||
in {
|
default = cfg.settings.port + 1;
|
||||||
type = types.str;
|
defaultText = lib.literalExpression "config.${opt.settings.port} + 1";
|
||||||
default = "http+unix://${replaceStrings ["/"] ["%2F"] sockPath}";
|
description = ''
|
||||||
defaultText = literalExpression ''
|
Socket.io port for EPGStation to listen on. It is valid to share
|
||||||
"http+unix://''${replaceStrings ["/"] ["%2F"] config.${options.services.mirakurun.unixSocket}}"
|
ports with <option>${opt.settings.port}</option>.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
options.clientSocketioPort = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = cfg.settings.socketioPort;
|
||||||
|
defaultText = lib.literalExpression "config.${opt.settings.socketioPort}";
|
||||||
|
description = ''
|
||||||
|
Socket.io port that the web client is going to connect to. This may
|
||||||
|
be different from <option>${opt.settings.socketioPort}</option> if
|
||||||
|
EPGStation is hidden behind a reverse proxy.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
options.mirakurunPath = with mirakurun; lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "http+unix://${lib.replaceStrings ["/"] ["%2F"] sock}";
|
||||||
|
defaultText = lib.literalExpression ''
|
||||||
|
"http+unix://''${lib.replaceStrings ["/"] ["%2F"] config.${option}}"
|
||||||
'';
|
'';
|
||||||
example = "http://localhost:40772";
|
example = "http://localhost:40772";
|
||||||
description = "URL to connect to Mirakurun.";
|
description = "URL to connect to Mirakurun.";
|
||||||
});
|
};
|
||||||
|
|
||||||
options.encode = mkOption {
|
options.encodeProcessNum = lib.mkOption {
|
||||||
type = with types; listOf attrs;
|
type = lib.types.ints.positive;
|
||||||
|
default = 4;
|
||||||
|
description = ''
|
||||||
|
The maximum number of processes that EPGStation would allow to run
|
||||||
|
at the same time for encoding or streaming videos.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
options.concurrentEncodeNum = lib.mkOption {
|
||||||
|
type = lib.types.ints.positive;
|
||||||
|
default = 1;
|
||||||
|
description = ''
|
||||||
|
The maximum number of encoding jobs that EPGStation would run at the
|
||||||
|
same time.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
options.encode = lib.mkOption {
|
||||||
|
type = with lib.types; listOf attrs;
|
||||||
description = "Encoding presets for recorded videos.";
|
description = "Encoding presets for recorded videos.";
|
||||||
default = [
|
default = [
|
||||||
{
|
{
|
||||||
name = "H264";
|
name = "H.264";
|
||||||
cmd = "${pkgs.epgstation}/libexec/enc.sh main";
|
cmd = "%NODE% ${cfg.package}/libexec/enc.js";
|
||||||
suffix = ".mp4";
|
suffix = ".mp4";
|
||||||
default = true;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "H264-sub";
|
|
||||||
cmd = "${pkgs.epgstation}/libexec/enc.sh sub";
|
|
||||||
suffix = "-sub.mp4";
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
defaultText = literalExpression ''
|
defaultText = lib.literalExpression ''
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name = "H264";
|
name = "H.264";
|
||||||
cmd = "''${pkgs.epgstation}/libexec/enc.sh main";
|
cmd = "%NODE% config.${opt.package}/libexec/enc.js";
|
||||||
suffix = ".mp4";
|
suffix = ".mp4";
|
||||||
default = true;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "H264-sub";
|
|
||||||
cmd = "''${pkgs.epgstation}/libexec/enc.sh sub";
|
|
||||||
suffix = "-sub.mp4";
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
'';
|
'';
|
||||||
|
@ -229,14 +241,25 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = !(lib.hasAttr "readOnlyOnce" cfg.settings);
|
||||||
|
message = ''
|
||||||
|
The option config.${opt.settings}.readOnlyOnce can no longer be used
|
||||||
|
since it's been removed. No replacements are available.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
environment.etc = {
|
environment.etc = {
|
||||||
"epgstation/operatorLogConfig.json".text = builtins.toJSON logConfig;
|
"epgstation/epgUpdaterLogConfig.yml".source = logConfig;
|
||||||
"epgstation/serviceLogConfig.json".text = builtins.toJSON logConfig;
|
"epgstation/operatorLogConfig.yml".source = logConfig;
|
||||||
|
"epgstation/serviceLogConfig.yml".source = logConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall = mkIf cfg.openFirewall {
|
networking.firewall = lib.mkIf cfg.openFirewall {
|
||||||
allowedTCPPorts = with cfg; [ port socketioPort ];
|
allowedTCPPorts = with cfg.settings; [ port socketioPort ];
|
||||||
};
|
};
|
||||||
|
|
||||||
users.users.epgstation = {
|
users.users.epgstation = {
|
||||||
|
@ -245,13 +268,13 @@ in
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
users.groups.epgstation = {};
|
users.groups.epgstation = { };
|
||||||
|
|
||||||
services.mirakurun.enable = mkDefault true;
|
services.mirakurun.enable = lib.mkDefault true;
|
||||||
|
|
||||||
services.mysql = {
|
services.mysql = {
|
||||||
enable = mkDefault true;
|
enable = lib.mkDefault true;
|
||||||
package = mkDefault pkgs.mariadb;
|
package = lib.mkDefault pkgs.mariadb;
|
||||||
ensureDatabases = [ cfg.database.name ];
|
ensureDatabases = [ cfg.database.name ];
|
||||||
# FIXME: enable once mysqljs supports auth_socket
|
# FIXME: enable once mysqljs supports auth_socket
|
||||||
# ensureUsers = [ {
|
# ensureUsers = [ {
|
||||||
|
@ -260,39 +283,28 @@ in
|
||||||
# } ];
|
# } ];
|
||||||
};
|
};
|
||||||
|
|
||||||
services.epgstation.settings = let
|
services.epgstation.settings =
|
||||||
defaultSettings = {
|
let
|
||||||
serverPort = cfg.port;
|
defaultSettings = {
|
||||||
socketioPort = cfg.socketioPort;
|
dbtype = lib.mkDefault "mysql";
|
||||||
clientSocketioPort = cfg.clientSocketioPort;
|
mysql = {
|
||||||
|
socketPath = lib.mkDefault "/run/mysqld/mysqld.sock";
|
||||||
|
user = username;
|
||||||
|
password = lib.mkDefault "@dbPassword@";
|
||||||
|
database = cfg.database.name;
|
||||||
|
};
|
||||||
|
|
||||||
dbType = mkDefault "mysql";
|
ffmpeg = lib.mkDefault "${pkgs.ffmpeg-full}/bin/ffmpeg";
|
||||||
mysql = {
|
ffprobe = lib.mkDefault "${pkgs.ffmpeg-full}/bin/ffprobe";
|
||||||
user = username;
|
|
||||||
database = cfg.database.name;
|
# for disambiguation with TypeScript files
|
||||||
socketPath = mkDefault "/run/mysqld/mysqld.sock";
|
recordedFileExtension = lib.mkDefault ".m2ts";
|
||||||
password = mkDefault "@dbPassword@";
|
|
||||||
connectTimeout = mkDefault 1000;
|
|
||||||
connectionLimit = mkDefault 10;
|
|
||||||
};
|
};
|
||||||
|
in
|
||||||
basicAuth = mkIf (cfg.basicAuth.user != null) {
|
lib.mkMerge [
|
||||||
user = mkDefault cfg.basicAuth.user;
|
defaultSettings
|
||||||
password = mkDefault "@password@";
|
(lib.mkIf cfg.usePreconfiguredStreaming streamingConfig)
|
||||||
};
|
];
|
||||||
|
|
||||||
ffmpeg = mkDefault "${pkgs.ffmpeg-full}/bin/ffmpeg";
|
|
||||||
ffprobe = mkDefault "${pkgs.ffmpeg-full}/bin/ffprobe";
|
|
||||||
|
|
||||||
fileExtension = mkDefault ".m2ts";
|
|
||||||
maxEncode = mkDefault 2;
|
|
||||||
maxStreaming = mkDefault 2;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
mkMerge [
|
|
||||||
defaultSettings
|
|
||||||
(mkIf cfg.usePreconfiguredStreaming streamingConfig)
|
|
||||||
];
|
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d '/var/lib/epgstation/streamfiles' - ${username} ${groupname} - -"
|
"d '/var/lib/epgstation/streamfiles' - ${username} ${groupname} - -"
|
||||||
|
@ -301,15 +313,15 @@ in
|
||||||
];
|
];
|
||||||
|
|
||||||
systemd.services.epgstation = {
|
systemd.services.epgstation = {
|
||||||
description = pkgs.epgstation.meta.description;
|
inherit description;
|
||||||
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
after = [
|
after = [ "network.target" ]
|
||||||
"network.target"
|
++ lib.optional config.services.mirakurun.enable "mirakurun.service"
|
||||||
] ++ optional config.services.mirakurun.enable "mirakurun.service"
|
++ lib.optional config.services.mysql.enable "mysql.service";
|
||||||
++ optional config.services.mysql.enable "mysql.service";
|
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = "${pkgs.epgstation}/bin/epgstation start";
|
ExecStart = "${cfg.package}/bin/epgstation start";
|
||||||
ExecStartPre = "+${preStartScript}";
|
ExecStartPre = "+${preStartScript}";
|
||||||
User = username;
|
User = username;
|
||||||
Group = groupname;
|
Group = groupname;
|
||||||
|
|
|
@ -1,119 +1,140 @@
|
||||||
{
|
{
|
||||||
"liveHLS": [
|
"urlscheme": {
|
||||||
{
|
"m2ts": {
|
||||||
"name": "720p",
|
"ios": "vlc-x-callback://x-callback-url/stream?url=PROTOCOL://ADDRESS",
|
||||||
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
|
"android": "intent://ADDRESS#Intent;package=org.videolan.vlc;type=video;scheme=PROTOCOL;end"
|
||||||
},
|
},
|
||||||
{
|
"video": {
|
||||||
"name": "480p",
|
"ios": "infuse://x-callback-url/play?url=PROTOCOL://ADDRESS",
|
||||||
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%"
|
"android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=PROTOCOL;end"
|
||||||
},
|
},
|
||||||
{
|
"download": {
|
||||||
"name": "180p",
|
"ios": "vlc-x-callback://x-callback-url/download?url=PROTOCOL://ADDRESS&filename=FILENAME"
|
||||||
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 48k -ac 2 -c:v libx264 -vf yadif,scale=-2:180 -b:v 100k -preset veryfast -maxrate 110k -bufsize 1000k -flags +loop-global_header %OUTPUT%"
|
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"liveMP4": [
|
|
||||||
{
|
|
||||||
"name": "720p",
|
|
||||||
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "480p",
|
|
||||||
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"liveWebM": [
|
|
||||||
{
|
|
||||||
"name": "720p",
|
|
||||||
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "480p",
|
|
||||||
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"mpegTsStreaming": [
|
|
||||||
{
|
|
||||||
"name": "720p",
|
|
||||||
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -y -f mpegts pipe:1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "480p",
|
|
||||||
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -y -f mpegts pipe:1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Original"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"mpegTsViewer": {
|
|
||||||
"ios": "vlc-x-callback://x-callback-url/stream?url=http://ADDRESS",
|
|
||||||
"android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=http;end"
|
|
||||||
},
|
},
|
||||||
"recordedDownloader": {
|
"stream": {
|
||||||
"ios": "vlc-x-callback://x-callback-url/download?url=http://ADDRESS&filename=FILENAME",
|
"live": {
|
||||||
"android": "intent://ADDRESS#Intent;package=com.dv.adm;type=video;scheme=http;end"
|
"ts": {
|
||||||
},
|
"m2ts": [
|
||||||
"recordedStreaming": {
|
{
|
||||||
"webm": [
|
"name": "720p",
|
||||||
{
|
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -y -f mpegts pipe:1"
|
||||||
"name": "720p",
|
},
|
||||||
"cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1",
|
{
|
||||||
"vb": "3000k",
|
"name": "480p",
|
||||||
"ab": "192k"
|
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -y -f mpegts pipe:1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "360p",
|
"name": "無変換"
|
||||||
"cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1",
|
}
|
||||||
"vb": "1500k",
|
],
|
||||||
"ab": "128k"
|
"m2tsll": [
|
||||||
|
{
|
||||||
|
"name": "720p",
|
||||||
|
"cmd": "%FFMPEG% -dual_mono_mode main -f mpegts -analyzeduration 500000 -i pipe:0 -map 0 -c:s copy -c:d copy -ignore_unknown -fflags nobuffer -flags low_delay -max_delay 250000 -max_interleave_delta 1 -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -flags +cgop -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -y -f mpegts pipe:1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "480p",
|
||||||
|
"cmd": "%FFMPEG% -dual_mono_mode main -f mpegts -analyzeduration 500000 -i pipe:0 -map 0 -c:s copy -c:d copy -ignore_unknown -fflags nobuffer -flags low_delay -max_delay 250000 -max_interleave_delta 1 -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -flags +cgop -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -y -f mpegts pipe:1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"webm": [
|
||||||
|
{
|
||||||
|
"name": "720p",
|
||||||
|
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "480p",
|
||||||
|
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mp4": [
|
||||||
|
{
|
||||||
|
"name": "720p",
|
||||||
|
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "480p",
|
||||||
|
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hls": [
|
||||||
|
{
|
||||||
|
"name": "720p",
|
||||||
|
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "480p",
|
||||||
|
"cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"mp4": [
|
|
||||||
{
|
|
||||||
"name": "720p",
|
|
||||||
"cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1",
|
|
||||||
"vb": "3000k",
|
|
||||||
"ab": "192k"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "360p",
|
|
||||||
"cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1",
|
|
||||||
"vb": "1500k",
|
|
||||||
"ab": "128k"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"mpegTs": [
|
|
||||||
{
|
|
||||||
"name": "720p (H.264)",
|
|
||||||
"cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -y -f mpegts pipe:1",
|
|
||||||
"vb": "3000k",
|
|
||||||
"ab": "192k"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "360p (H.264)",
|
|
||||||
"cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -y -f mpegts pipe:1",
|
|
||||||
"vb": "1500k",
|
|
||||||
"ab": "128k"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"recordedHLS": [
|
|
||||||
{
|
|
||||||
"name": "720p",
|
|
||||||
"cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
|
|
||||||
},
|
},
|
||||||
{
|
"recorded": {
|
||||||
"name": "480p",
|
"ts": {
|
||||||
"cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%"
|
"webm": [
|
||||||
},
|
{
|
||||||
{
|
"name": "720p",
|
||||||
"name": "480p(h265)",
|
"cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
|
||||||
"cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_type fmp4 -hls_fmp4_init_filename stream%streamNum%-init.mp4 -hls_segment_filename stream%streamNum%-%09d.m4s -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx265 -vf yadif,scale=-2:480 -b:v 350k -preset veryfast -tag:v hvc1 %OUTPUT%"
|
},
|
||||||
|
{
|
||||||
|
"name": "480p",
|
||||||
|
"cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mp4": [
|
||||||
|
{
|
||||||
|
"name": "720p",
|
||||||
|
"cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "480p",
|
||||||
|
"cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hls": [
|
||||||
|
{
|
||||||
|
"name": "720p",
|
||||||
|
"cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "480p",
|
||||||
|
"cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"encoded": {
|
||||||
|
"webm": [
|
||||||
|
{
|
||||||
|
"name": "720p",
|
||||||
|
"cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "480p",
|
||||||
|
"cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mp4": [
|
||||||
|
{
|
||||||
|
"name": "720p",
|
||||||
|
"cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "480p",
|
||||||
|
"cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hls": [
|
||||||
|
{
|
||||||
|
"name": "720p",
|
||||||
|
"cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "480p",
|
||||||
|
"cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf scale=-2:480 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"recordedViewer": {
|
|
||||||
"ios": "infuse://x-callback-url/play?url=http://ADDRESS",
|
|
||||||
"android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=http;end"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue