Merge pull request #121 from jb-alvarado/master
updates and optimizations
This commit is contained in:
commit
6f66cc5533
142
Cargo.lock
generated
142
Cargo.lock
generated
@ -88,15 +88,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"time",
|
"time 0.1.43",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "3.1.12"
|
version = "3.1.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c167e37342afc5f33fd87bbc870cedd020d2a6dffa05d45ccd9241fbdd146db"
|
checksum = "85a35a599b11c089a7f49105658d089b8f2cf0882993c17daf6de15285c2c35d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atty",
|
"atty",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
@ -124,9 +124,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_lex"
|
||||||
version = "0.1.1"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "189ddd3b5d32a70b35e7686054371742a937b0d99128e76dde6340210e966669"
|
checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"os_str_bytes",
|
"os_str_bytes",
|
||||||
]
|
]
|
||||||
@ -196,7 +196,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ffplayout-engine"
|
name = "ffplayout-engine"
|
||||||
version = "0.9.4"
|
version = "0.9.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
@ -216,6 +216,7 @@ dependencies = [
|
|||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"shlex",
|
"shlex",
|
||||||
"simplelog",
|
"simplelog",
|
||||||
|
"time 0.3.9",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -232,8 +233,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "file-rotate"
|
name = "file-rotate"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/jb-alvarado/file-rotate.git#ae5062a5b82626b4d1f9fea2a17325fe1d160d4c"
|
||||||
checksum = "f8071df7315b1cd4006ce687043f393cca212761889b3626c1444ae06e8f72d0"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"flate2",
|
"flate2",
|
||||||
@ -466,9 +466,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.6"
|
version = "0.2.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"
|
checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@ -488,9 +488,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.7.0"
|
version = "1.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba"
|
checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpdate"
|
name = "httpdate"
|
||||||
@ -659,9 +659,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lettre"
|
name = "lettre"
|
||||||
version = "0.10.0-rc.5"
|
version = "0.10.0-rc.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5144148f337be14dabfc0f0d85b691a68ac6c77ef22a5c47c5504b70a7c9fcf3"
|
checksum = "2f6c70001f7ee6c93b6687a06607c7a38f9a7ae460139a496c23da21e95bc289"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"email-encoding",
|
"email-encoding",
|
||||||
@ -680,9 +680,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.124"
|
version = "0.2.125"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50"
|
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linked-hash-map"
|
name = "linked-hash-map"
|
||||||
@ -702,9 +702,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.16"
|
version = "0.4.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
|
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
]
|
]
|
||||||
@ -723,9 +723,9 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.4.1"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
@ -882,9 +882,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.44"
|
version = "0.1.45"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
|
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
@ -892,9 +892,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.14"
|
version = "0.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
|
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
@ -909,6 +909,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_threads"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
@ -917,18 +926,30 @@ checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.38"
|
version = "0.10.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"
|
checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"foreign-types",
|
"foreign-types",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"openssl-macros",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "openssl-probe"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@ -946,9 +967,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.72"
|
version = "0.9.73"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
|
checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"cc",
|
"cc",
|
||||||
@ -966,9 +987,9 @@ checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paris"
|
name = "paris"
|
||||||
version = "1.5.11"
|
version = "1.5.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c69d19a208bba8b94bd27d4b7a06ad153cddc6b88cb2149a668e23ce7bdb67d5"
|
checksum = "2eaf2319cd71dd9ff38c72bebde61b9ea657134abcf26ae4205f54f772a32810"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
@ -997,9 +1018,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.8"
|
version = "0.2.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
|
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-utils"
|
name = "pin-utils"
|
||||||
@ -1188,18 +1209,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.136"
|
version = "1.0.137"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
|
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.136"
|
version = "1.0.137"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
|
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1208,9 +1229,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.79"
|
version = "1.0.81"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
|
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
@ -1219,9 +1240,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_yaml"
|
name = "serde_yaml"
|
||||||
version = "0.8.23"
|
version = "0.8.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0"
|
checksum = "707d15895415db6628332b737c838b88c598522e4dc70647e59b72312924aebc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"ryu",
|
"ryu",
|
||||||
@ -1237,14 +1258,14 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simplelog"
|
name = "simplelog"
|
||||||
version = "0.11.2"
|
version = "0.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c1348164456f72ca0116e4538bdaabb0ddb622c7d9f16387c725af3e96d6001c"
|
checksum = "48dfff04aade74dd495b007c831cd6f4e0cee19c344dd9dc0884c0289b70a786"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
|
||||||
"log",
|
"log",
|
||||||
"paris",
|
"paris",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
|
"time 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1277,9 +1298,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.91"
|
version = "1.0.92"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d"
|
checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1325,6 +1346,24 @@ dependencies = [
|
|||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"libc",
|
||||||
|
"num_threads",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@ -1342,15 +1381,16 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.17.0"
|
version = "1.18.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee"
|
checksum = "dce653fb475565de9f6fb0614b28bca8df2c430c0cf84bcd9c843f15de5414cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mio 0.8.2",
|
"mio 0.8.2",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
|
"once_cell",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
@ -1424,9 +1464,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.7"
|
version = "0.3.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
|
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-normalization"
|
name = "unicode-normalization"
|
||||||
@ -1439,9 +1479,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.2"
|
version = "0.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
|
12
Cargo.toml
12
Cargo.toml
@ -4,7 +4,7 @@ description = "24/7 playout based on rust and ffmpeg"
|
|||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
authors = ["Jonathan Baecker jonbae77@gmail.com"]
|
authors = ["Jonathan Baecker jonbae77@gmail.com"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
version = "0.9.4"
|
version = "0.9.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@ -12,9 +12,9 @@ chrono = "0.4"
|
|||||||
clap = { version = "3.1", features = ["derive"] }
|
clap = { version = "3.1", features = ["derive"] }
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
ffprobe = "0.3"
|
ffprobe = "0.3"
|
||||||
file-rotate = "0.6"
|
file-rotate = { git = "https://github.com/jb-alvarado/file-rotate.git" }
|
||||||
jsonrpc-http-server = "18.0"
|
jsonrpc-http-server = "18.0"
|
||||||
lettre = "0.10.0-rc.5"
|
lettre = "0.10.0-rc.6"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
notify = "4.0"
|
notify = "4.0"
|
||||||
once_cell = "1.10"
|
once_cell = "1.10"
|
||||||
@ -24,7 +24,8 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.8"
|
serde_yaml = "0.8"
|
||||||
shlex = "1.1"
|
shlex = "1.1"
|
||||||
simplelog = { version = "^0.11", features = ["paris"] }
|
simplelog = { version = "^0.12", features = ["paris"] }
|
||||||
|
time = { version = "0.3", features = ["formatting", "macros"] }
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
|
|
||||||
[target.x86_64-unknown-linux-musl.dependencies]
|
[target.x86_64-unknown-linux-musl.dependencies]
|
||||||
@ -48,6 +49,7 @@ license-file = ["LICENSE", "0"]
|
|||||||
depends = ""
|
depends = ""
|
||||||
suggests = "ffmpeg"
|
suggests = "ffmpeg"
|
||||||
copyright = "Copyright (c) 2022, Jonathan Baecker. All rights reserved."
|
copyright = "Copyright (c) 2022, Jonathan Baecker. All rights reserved."
|
||||||
|
conf-files = ["/etc/ffplayout/ffplayout.yml"]
|
||||||
assets = [
|
assets = [
|
||||||
["target/x86_64-unknown-linux-musl/release/ffplayout", "/usr/bin/ffplayout", "755"],
|
["target/x86_64-unknown-linux-musl/release/ffplayout", "/usr/bin/ffplayout", "755"],
|
||||||
["assets/ffplayout.yml", "/etc/ffplayout/ffplayout.yml", "644"],
|
["assets/ffplayout.yml", "/etc/ffplayout/ffplayout.yml", "644"],
|
||||||
@ -62,7 +64,7 @@ name = "ffplayout-engine"
|
|||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
assets = [
|
assets = [
|
||||||
{ source = "target/x86_64-unknown-linux-musl/release/ffplayout", dest = "/usr/bin/ffplayout", mode = "755" },
|
{ source = "target/x86_64-unknown-linux-musl/release/ffplayout", dest = "/usr/bin/ffplayout", mode = "755" },
|
||||||
{ source = "assets/ffplayout.yml", dest = "/etc/ffplayout/ffplayout.yml", mode = "644" },
|
{ source = "assets/ffplayout.yml", dest = "/etc/ffplayout/ffplayout.yml", mode = "644", config = true },
|
||||||
{ source = "assets/ffplayout-engine.service", dest = "/lib/systemd/system/ffplayout-engine.service", mode = "644" },
|
{ source = "assets/ffplayout-engine.service", dest = "/lib/systemd/system/ffplayout-engine.service", mode = "644" },
|
||||||
{ source = "README.md", dest = "/usr/share/doc/ffplayout-engine/README", mode = "644", doc = true },
|
{ source = "README.md", dest = "/usr/share/doc/ffplayout-engine/README", mode = "644", doc = true },
|
||||||
{ source = "LICENSE", dest = "/usr/share/doc/ffplayout-engine/LICENSE", mode = "644" },
|
{ source = "LICENSE", dest = "/usr/share/doc/ffplayout-engine/LICENSE", mode = "644" },
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Rust based 24/7 playout solution
|
Description=Rust based 24/7 playout solution
|
||||||
After=network.target
|
After=network.target remote-fs.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart= /usr/bin/ffplayout
|
ExecStart= /usr/bin/ffplayout
|
||||||
|
@ -62,6 +62,7 @@ processing:
|
|||||||
logo_opacity: 0.7
|
logo_opacity: 0.7
|
||||||
logo_filter: overlay=W-w-12:12
|
logo_filter: overlay=W-w-12:12
|
||||||
add_loudnorm: false
|
add_loudnorm: false
|
||||||
|
loudnorm_ingest: false
|
||||||
loud_i: -18
|
loud_i: -18
|
||||||
loud_tp: -1.5
|
loud_tp: -1.5
|
||||||
loud_lra: 11
|
loud_lra: 11
|
||||||
|
11
src/filter/a_loudnorm.rs
Normal file
11
src/filter/a_loudnorm.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
use crate::utils::GlobalConfig;
|
||||||
|
|
||||||
|
/// Loudnorm Audio Filter
|
||||||
|
///
|
||||||
|
/// Add loudness normalization.
|
||||||
|
pub fn filter_node(config: &GlobalConfig) -> String {
|
||||||
|
format!(
|
||||||
|
",loudnorm=I={}:TP={}:LRA={}",
|
||||||
|
config.processing.loud_i, config.processing.loud_tp, config.processing.loud_lra
|
||||||
|
)
|
||||||
|
}
|
53
src/filter/ingest_filter.rs
Normal file
53
src/filter/ingest_filter.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
use crate::filter::{a_loudnorm, v_overlay};
|
||||||
|
use crate::utils::GlobalConfig;
|
||||||
|
|
||||||
|
/// Audio Filter
|
||||||
|
///
|
||||||
|
/// If needed we add audio filters to the server instance.
|
||||||
|
fn audio_filter(config: &GlobalConfig) -> String {
|
||||||
|
let mut audio_chain = ";[0:a]afade=in:st=0:d=0.5".to_string();
|
||||||
|
|
||||||
|
if config.processing.loudnorm_ingest {
|
||||||
|
audio_chain.push_str(&a_loudnorm::filter_node(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.processing.volume != 1.0 {
|
||||||
|
audio_chain.push_str(format!(",volume={}", config.processing.volume).as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
audio_chain.push_str("[aout1]");
|
||||||
|
|
||||||
|
audio_chain
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create filter nodes for ingest live stream.
|
||||||
|
pub fn filter_cmd() -> Vec<String> {
|
||||||
|
let config = GlobalConfig::global();
|
||||||
|
|
||||||
|
let mut filter = format!(
|
||||||
|
"[0:v]fps={},scale={}:{},setdar=dar={},fade=in:st=0:d=0.5",
|
||||||
|
config.processing.fps,
|
||||||
|
config.processing.width,
|
||||||
|
config.processing.height,
|
||||||
|
config.processing.aspect
|
||||||
|
);
|
||||||
|
|
||||||
|
let overlay = v_overlay::filter_node(config, true);
|
||||||
|
|
||||||
|
if !overlay.is_empty() {
|
||||||
|
filter.push(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.push_str(&overlay);
|
||||||
|
filter.push_str("[vout1]");
|
||||||
|
filter.push_str(audio_filter(config).as_str());
|
||||||
|
|
||||||
|
vec![
|
||||||
|
"-filter_complex".to_string(),
|
||||||
|
filter,
|
||||||
|
"-map".to_string(),
|
||||||
|
"[vout1]".to_string(),
|
||||||
|
"-map".to_string(),
|
||||||
|
"[aout1]".to_string(),
|
||||||
|
]
|
||||||
|
}
|
@ -2,7 +2,10 @@ use std::path::Path;
|
|||||||
|
|
||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
|
|
||||||
|
pub mod a_loudnorm;
|
||||||
|
pub mod ingest_filter;
|
||||||
pub mod v_drawtext;
|
pub mod v_drawtext;
|
||||||
|
pub mod v_overlay;
|
||||||
|
|
||||||
use crate::utils::{get_delta, is_close, GlobalConfig, Media};
|
use crate::utils::{get_delta, is_close, GlobalConfig, Media};
|
||||||
|
|
||||||
@ -28,7 +31,7 @@ impl Filters {
|
|||||||
match codec_type {
|
match codec_type {
|
||||||
"audio" => match &self.audio_chain {
|
"audio" => match &self.audio_chain {
|
||||||
Some(ac) => {
|
Some(ac) => {
|
||||||
if filter.starts_with(";") || filter.starts_with("[") {
|
if filter.starts_with(';') || filter.starts_with('[') {
|
||||||
self.audio_chain = Some(format!("{ac}{filter}"))
|
self.audio_chain = Some(format!("{ac}{filter}"))
|
||||||
} else {
|
} else {
|
||||||
self.audio_chain = Some(format!("{ac},{filter}"))
|
self.audio_chain = Some(format!("{ac},{filter}"))
|
||||||
@ -46,7 +49,7 @@ impl Filters {
|
|||||||
},
|
},
|
||||||
"video" => match &self.video_chain {
|
"video" => match &self.video_chain {
|
||||||
Some(vc) => {
|
Some(vc) => {
|
||||||
if filter.starts_with(";") || filter.starts_with("[") {
|
if filter.starts_with(';') || filter.starts_with('[') {
|
||||||
self.video_chain = Some(format!("{vc}{filter}"))
|
self.video_chain = Some(format!("{vc}{filter}"))
|
||||||
} else {
|
} else {
|
||||||
self.video_chain = Some(format!("{vc},{filter}"))
|
self.video_chain = Some(format!("{vc},{filter}"))
|
||||||
@ -62,9 +65,9 @@ impl Filters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deinterlace(field_order: Option<String>, chain: &mut Filters) {
|
fn deinterlace(field_order: &Option<String>, chain: &mut Filters) {
|
||||||
if let Some(order) = field_order {
|
if let Some(order) = field_order {
|
||||||
if &order != "progressive" {
|
if order != "progressive" {
|
||||||
chain.add_filter("yadif=0:-1:0", "video")
|
chain.add_filter("yadif=0:-1:0", "video")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,10 +97,7 @@ fn pad(aspect: f64, chain: &mut Filters, config: &GlobalConfig) {
|
|||||||
|
|
||||||
fn fps(fps: f64, chain: &mut Filters, config: &GlobalConfig) {
|
fn fps(fps: f64, chain: &mut Filters, config: &GlobalConfig) {
|
||||||
if fps != config.processing.fps {
|
if fps != config.processing.fps {
|
||||||
chain.add_filter(
|
chain.add_filter(&format!("fps={}", config.processing.fps), "video")
|
||||||
&format!("fps={}", config.processing.fps),
|
|
||||||
"video",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,10 +113,7 @@ fn scale(width: i64, height: i64, aspect: f64, chain: &mut Filters, config: &Glo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !is_close(aspect, config.processing.aspect, 0.03) {
|
if !is_close(aspect, config.processing.aspect, 0.03) {
|
||||||
chain.add_filter(
|
chain.add_filter(&format!("setdar=dar={}", config.processing.aspect), "video")
|
||||||
&format!("setdar=dar={}", config.processing.aspect),
|
|
||||||
"video"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,17 +139,9 @@ fn fade(node: &mut Media, chain: &mut Filters, codec_type: &str) {
|
|||||||
fn overlay(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) {
|
fn overlay(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) {
|
||||||
if config.processing.add_logo
|
if config.processing.add_logo
|
||||||
&& Path::new(&config.processing.logo).is_file()
|
&& Path::new(&config.processing.logo).is_file()
|
||||||
&& &node.category.clone().unwrap_or(String::new()) != "advertisement"
|
&& &node.category.clone().unwrap_or_default() != "advertisement"
|
||||||
{
|
{
|
||||||
let opacity = format!(
|
let mut logo_chain = v_overlay::filter_node(config, false);
|
||||||
"format=rgba,colorchannelmixer=aa={}",
|
|
||||||
config.processing.logo_opacity
|
|
||||||
);
|
|
||||||
let logo_loop = "loop=loop=-1:size=1:start=0";
|
|
||||||
let mut logo_chain = format!(
|
|
||||||
"null[v];movie={},{logo_loop},{opacity}",
|
|
||||||
config.processing.logo
|
|
||||||
);
|
|
||||||
|
|
||||||
if node.last_ad.unwrap() {
|
if node.last_ad.unwrap() {
|
||||||
logo_chain.push_str(",fade=in:st=0:d=1.0:alpha=1")
|
logo_chain.push_str(",fade=in:st=0:d=1.0:alpha=1")
|
||||||
@ -173,7 +162,7 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) {
|
|||||||
|
|
||||||
fn extend_video(node: &mut Media, chain: &mut Filters) {
|
fn extend_video(node: &mut Media, chain: &mut Filters) {
|
||||||
let video_streams = node.probe.clone().unwrap().video_streams.unwrap();
|
let video_streams = node.probe.clone().unwrap().video_streams.unwrap();
|
||||||
if video_streams.len() > 0 {
|
if !video_streams.is_empty() {
|
||||||
if let Some(duration) = &video_streams[0].duration {
|
if let Some(duration) = &video_streams[0].duration {
|
||||||
let duration_float = duration.clone().parse::<f64>().unwrap();
|
let duration_float = duration.clone().parse::<f64>().unwrap();
|
||||||
|
|
||||||
@ -190,16 +179,15 @@ fn extend_video(node: &mut Media, chain: &mut Filters) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// add drawtext filter for lower thirds messages
|
||||||
fn add_text(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) {
|
fn add_text(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) {
|
||||||
// add drawtext filter for lower thirds messages
|
|
||||||
|
|
||||||
if config.text.add_text && config.text.over_pre {
|
if config.text.add_text && config.text.over_pre {
|
||||||
let filter = v_drawtext::filter_node(node);
|
let filter = v_drawtext::filter_node(node);
|
||||||
|
|
||||||
chain.add_filter(&filter, "video");
|
chain.add_filter(&filter, "video");
|
||||||
|
|
||||||
if let Some(filters) = &chain.video_chain {
|
if let Some(filters) = &chain.video_chain {
|
||||||
for (i, f) in filters.split(",").enumerate() {
|
for (i, f) in filters.split(',').enumerate() {
|
||||||
if f.contains("drawtext") && !config.text.text_from_filename {
|
if f.contains("drawtext") && !config.text.text_from_filename {
|
||||||
debug!("drawtext node is on index: <yellow>{i}</>");
|
debug!("drawtext node is on index: <yellow>{i}</>");
|
||||||
break;
|
break;
|
||||||
@ -211,7 +199,7 @@ fn add_text(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) {
|
|||||||
|
|
||||||
fn add_audio(node: &mut Media, chain: &mut Filters) {
|
fn add_audio(node: &mut Media, chain: &mut Filters) {
|
||||||
let audio_streams = node.probe.clone().unwrap().audio_streams.unwrap();
|
let audio_streams = node.probe.clone().unwrap().audio_streams.unwrap();
|
||||||
if audio_streams.len() == 0 {
|
if audio_streams.is_empty() {
|
||||||
warn!("Clip: '{}' has no audio!", node.source);
|
warn!("Clip: '{}' has no audio!", node.source);
|
||||||
let audio = format!(
|
let audio = format!(
|
||||||
"aevalsrc=0:channel_layout=stereo:duration={}:sample_rate=48000",
|
"aevalsrc=0:channel_layout=stereo:duration={}:sample_rate=48000",
|
||||||
@ -223,55 +211,54 @@ fn add_audio(node: &mut Media, chain: &mut Filters) {
|
|||||||
|
|
||||||
fn extend_audio(node: &mut Media, chain: &mut Filters) {
|
fn extend_audio(node: &mut Media, chain: &mut Filters) {
|
||||||
let audio_streams = node.probe.clone().unwrap().audio_streams.unwrap();
|
let audio_streams = node.probe.clone().unwrap().audio_streams.unwrap();
|
||||||
if audio_streams.len() > 0 {
|
if !audio_streams.is_empty() {
|
||||||
if let Some(duration) = &audio_streams[0].duration {
|
if let Some(duration) = &audio_streams[0].duration {
|
||||||
let duration_float = duration.clone().parse::<f64>().unwrap();
|
let duration_float = duration.clone().parse::<f64>().unwrap();
|
||||||
|
|
||||||
if node.out - node.seek > duration_float - node.seek + 0.1 {
|
if node.out - node.seek > duration_float - node.seek + 0.1 {
|
||||||
chain.add_filter(
|
chain.add_filter(&format!("apad=whole_dur={}", node.out - node.seek), "audio")
|
||||||
&format!("apad=whole_dur={}", node.out - node.seek),
|
|
||||||
"audio",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add single pass loudnorm filter to audio line.
|
||||||
fn add_loudnorm(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) {
|
fn add_loudnorm(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) {
|
||||||
// add single pass loudnorm filter to audio line
|
|
||||||
|
|
||||||
if node.probe.is_some()
|
if node.probe.is_some()
|
||||||
&& node.probe.clone().unwrap().audio_streams.unwrap().len() > 0
|
&& !node
|
||||||
|
.probe
|
||||||
|
.clone()
|
||||||
|
.unwrap()
|
||||||
|
.audio_streams
|
||||||
|
.unwrap()
|
||||||
|
.is_empty()
|
||||||
&& config.processing.add_loudnorm
|
&& config.processing.add_loudnorm
|
||||||
{
|
{
|
||||||
let loud_filter = format!(
|
let loud_filter = a_loudnorm::filter_node(config);
|
||||||
"loudnorm=I={}:TP={}:LRA={}",
|
|
||||||
config.processing.loud_i, config.processing.loud_tp, config.processing.loud_lra
|
|
||||||
);
|
|
||||||
|
|
||||||
chain.add_filter(&loud_filter, "audio");
|
chain.add_filter(&loud_filter, "audio");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn audio_volume(chain: &mut Filters, config: &GlobalConfig) {
|
fn audio_volume(chain: &mut Filters, config: &GlobalConfig) {
|
||||||
if config.processing.volume != 1.0 {
|
if config.processing.volume != 1.0 {
|
||||||
chain.add_filter(
|
chain.add_filter(&format!("volume={}", config.processing.volume), "audio")
|
||||||
&format!("volume={}", config.processing.volume),
|
|
||||||
"audio",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn aspect_calc(aspect_string: String) -> f64 {
|
fn aspect_calc(aspect_string: &Option<String>, config: &GlobalConfig) -> f64 {
|
||||||
let aspect_vec: Vec<&str> = aspect_string.split(':').collect();
|
let mut source_aspect = config.processing.aspect;
|
||||||
let w: f64 = aspect_vec[0].parse().unwrap();
|
|
||||||
let h: f64 = aspect_vec[1].parse().unwrap();
|
if let Some(aspect) = aspect_string {
|
||||||
let source_aspect: f64 = w as f64 / h as f64;
|
let aspect_vec: Vec<&str> = aspect.split(':').collect();
|
||||||
|
let w: f64 = aspect_vec[0].parse().unwrap();
|
||||||
|
let h: f64 = aspect_vec[1].parse().unwrap();
|
||||||
|
source_aspect = w as f64 / h as f64;
|
||||||
|
}
|
||||||
|
|
||||||
source_aspect
|
source_aspect
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fps_calc(r_frame_rate: String) -> f64 {
|
fn fps_calc(r_frame_rate: &str) -> f64 {
|
||||||
let frame_rate_vec: Vec<&str> = r_frame_rate.split('/').collect();
|
let frame_rate_vec: Vec<&str> = r_frame_rate.split('/').collect();
|
||||||
let rate: f64 = frame_rate_vec[0].parse().unwrap();
|
let rate: f64 = frame_rate_vec[0].parse().unwrap();
|
||||||
let factor: f64 = frame_rate_vec[1].parse().unwrap();
|
let factor: f64 = frame_rate_vec[1].parse().unwrap();
|
||||||
@ -280,14 +267,8 @@ fn fps_calc(r_frame_rate: String) -> f64 {
|
|||||||
fps
|
fps
|
||||||
}
|
}
|
||||||
|
|
||||||
fn realtime_filter(
|
/// This realtime filter is important for HLS output to stay in sync.
|
||||||
node: &mut Media,
|
fn realtime_filter(node: &mut Media, chain: &mut Filters, config: &GlobalConfig, codec_type: &str) {
|
||||||
chain: &mut Filters,
|
|
||||||
config: &GlobalConfig,
|
|
||||||
codec_type: &str,
|
|
||||||
) {
|
|
||||||
// this realtime filter is important for HLS output to stay in sync
|
|
||||||
|
|
||||||
let mut t = "";
|
let mut t = "";
|
||||||
|
|
||||||
if codec_type == "audio" {
|
if codec_type == "audio" {
|
||||||
@ -325,18 +306,18 @@ pub fn filter_chains(node: &mut Media) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let v_stream = &probe.video_streams.unwrap()[0];
|
let v_stream = &probe.video_streams.unwrap()[0];
|
||||||
let aspect = aspect_calc(v_stream.display_aspect_ratio.clone().unwrap());
|
let aspect = aspect_calc(&v_stream.display_aspect_ratio, config);
|
||||||
let frame_per_sec = fps_calc(v_stream.r_frame_rate.clone());
|
let frame_per_sec = fps_calc(&v_stream.r_frame_rate);
|
||||||
|
|
||||||
deinterlace(v_stream.field_order.clone(), &mut filters);
|
deinterlace(&v_stream.field_order, &mut filters);
|
||||||
pad(aspect, &mut filters, &config);
|
pad(aspect, &mut filters, config);
|
||||||
fps(frame_per_sec, &mut filters, &config);
|
fps(frame_per_sec, &mut filters, config);
|
||||||
scale(
|
scale(
|
||||||
v_stream.width.unwrap(),
|
v_stream.width.unwrap(),
|
||||||
v_stream.height.unwrap(),
|
v_stream.height.unwrap(),
|
||||||
aspect,
|
aspect,
|
||||||
&mut filters,
|
&mut filters,
|
||||||
&config,
|
config,
|
||||||
);
|
);
|
||||||
extend_video(node, &mut filters);
|
extend_video(node, &mut filters);
|
||||||
|
|
||||||
@ -344,15 +325,15 @@ pub fn filter_chains(node: &mut Media) -> Vec<String> {
|
|||||||
extend_audio(node, &mut filters);
|
extend_audio(node, &mut filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
add_text(node, &mut filters, &config);
|
add_text(node, &mut filters, config);
|
||||||
fade(node, &mut filters, "video".into());
|
fade(node, &mut filters, "video");
|
||||||
overlay(node, &mut filters, &config);
|
overlay(node, &mut filters, config);
|
||||||
realtime_filter(node, &mut filters, &config, "video".into());
|
realtime_filter(node, &mut filters, config, "video");
|
||||||
|
|
||||||
add_loudnorm(node, &mut filters, &config);
|
add_loudnorm(node, &mut filters, config);
|
||||||
fade(node, &mut filters, "audio".into());
|
fade(node, &mut filters, "audio");
|
||||||
audio_volume(&mut filters, &config);
|
audio_volume(&mut filters, config);
|
||||||
realtime_filter(node, &mut filters, &config, "audio".into());
|
realtime_filter(node, &mut filters, config, "audio");
|
||||||
|
|
||||||
let mut filter_cmd = vec![];
|
let mut filter_cmd = vec![];
|
||||||
let mut filter_str: String = String::new();
|
let mut filter_str: String = String::new();
|
||||||
@ -368,7 +349,7 @@ pub fn filter_chains(node: &mut Media) -> Vec<String> {
|
|||||||
|
|
||||||
if let Some(a_filters) = filters.audio_chain {
|
if let Some(a_filters) = filters.audio_chain {
|
||||||
if filter_str.len() > 10 {
|
if filter_str.len() > 10 {
|
||||||
filter_str.push_str(";")
|
filter_str.push(';')
|
||||||
}
|
}
|
||||||
filter_str.push_str(a_filters.as_str());
|
filter_str.push_str(a_filters.as_str());
|
||||||
filter_str.push_str(filters.audio_map.clone().unwrap().as_str());
|
filter_str.push_str(filters.audio_map.clone().unwrap().as_str());
|
||||||
|
@ -24,14 +24,14 @@ pub fn filter_node(node: &mut Media) -> String {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let escape = text
|
let escape = text
|
||||||
.replace("'", "'\\\\\\''")
|
.replace('\'', "'\\\\\\''")
|
||||||
.replace("%", "\\\\\\%")
|
.replace('%', "\\\\\\%")
|
||||||
.replace(":", "\\:");
|
.replace(':', "\\:");
|
||||||
filter = format!("drawtext=text='{escape}':{}{font}", config.text.style)
|
filter = format!("drawtext=text='{escape}':{}{font}", config.text.style)
|
||||||
} else {
|
} else {
|
||||||
filter = format!(
|
filter = format!(
|
||||||
"zmq=b=tcp\\\\://'{}',drawtext=text=''{font}",
|
"zmq=b=tcp\\\\://'{}',drawtext=text=''{font}",
|
||||||
config.text.bind_address.replace(":", "\\:")
|
config.text.bind_address.replace(':', "\\:")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
30
src/filter/v_overlay.rs
Normal file
30
src/filter/v_overlay.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::utils::GlobalConfig;
|
||||||
|
|
||||||
|
/// Overlay Filter
|
||||||
|
///
|
||||||
|
/// When a logo is set, we create here the filter for the server.
|
||||||
|
pub fn filter_node(config: &GlobalConfig, add_tail: bool) -> String {
|
||||||
|
let mut logo_chain = String::new();
|
||||||
|
|
||||||
|
if config.processing.add_logo && Path::new(&config.processing.logo).is_file() {
|
||||||
|
let opacity = format!(
|
||||||
|
"format=rgba,colorchannelmixer=aa={}",
|
||||||
|
config.processing.logo_opacity
|
||||||
|
);
|
||||||
|
let logo_loop = "loop=loop=-1:size=1:start=0";
|
||||||
|
logo_chain = format!(
|
||||||
|
"null[v];movie={},{logo_loop},{opacity}",
|
||||||
|
config.processing.logo
|
||||||
|
);
|
||||||
|
|
||||||
|
if add_tail {
|
||||||
|
logo_chain.push_str(
|
||||||
|
format!("[l];[v][l]{}:shortest=1", config.processing.logo_filter).as_str(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logo_chain
|
||||||
|
}
|
@ -21,15 +21,18 @@ use walkdir::WalkDir;
|
|||||||
|
|
||||||
use crate::utils::{get_sec, GlobalConfig, Media};
|
use crate::utils::{get_sec, GlobalConfig, Media};
|
||||||
|
|
||||||
|
/// Folder Sources
|
||||||
|
///
|
||||||
|
/// Like playlist source, we create here a folder list for iterate over it.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Source {
|
pub struct FolderSource {
|
||||||
config: GlobalConfig,
|
config: GlobalConfig,
|
||||||
pub nodes: Arc<Mutex<Vec<Media>>>,
|
pub nodes: Arc<Mutex<Vec<Media>>>,
|
||||||
current_node: Media,
|
current_node: Media,
|
||||||
index: Arc<AtomicUsize>,
|
index: Arc<AtomicUsize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Source {
|
impl FolderSource {
|
||||||
pub fn new(current_list: Arc<Mutex<Vec<Media>>>, global_index: Arc<AtomicUsize>) -> Self {
|
pub fn new(current_list: Arc<Mutex<Vec<Media>>>, global_index: Arc<AtomicUsize>) -> Self {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
let mut media_list = vec![];
|
let mut media_list = vec![];
|
||||||
@ -90,12 +93,9 @@ impl Source {
|
|||||||
fn shuffle(&mut self) {
|
fn shuffle(&mut self) {
|
||||||
let mut rng = thread_rng();
|
let mut rng = thread_rng();
|
||||||
self.nodes.lock().unwrap().shuffle(&mut rng);
|
self.nodes.lock().unwrap().shuffle(&mut rng);
|
||||||
let mut index: usize = 0;
|
|
||||||
|
|
||||||
for item in self.nodes.lock().unwrap().iter_mut() {
|
for (index, item) in self.nodes.lock().unwrap().iter_mut().enumerate() {
|
||||||
item.index = Some(index);
|
item.index = Some(index);
|
||||||
|
|
||||||
index += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,17 +104,15 @@ impl Source {
|
|||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.sort_by(|d1, d2| d1.source.cmp(&d2.source));
|
.sort_by(|d1, d2| d1.source.cmp(&d2.source));
|
||||||
let mut index: usize = 0;
|
|
||||||
|
|
||||||
for item in self.nodes.lock().unwrap().iter_mut() {
|
for (index, item) in self.nodes.lock().unwrap().iter_mut().enumerate() {
|
||||||
item.index = Some(index);
|
item.index = Some(index);
|
||||||
|
|
||||||
index += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Iterator for Source {
|
/// Create iterator for folder source
|
||||||
|
impl Iterator for FolderSource {
|
||||||
type Item = Media;
|
type Item = Media;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
@ -159,6 +157,9 @@ fn file_extension(filename: &Path) -> Option<&str> {
|
|||||||
filename.extension().and_then(OsStr::to_str)
|
filename.extension().and_then(OsStr::to_str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a watcher, which monitor file changes.
|
||||||
|
/// When a change is register, update the current file list.
|
||||||
|
/// This makes it possible, to play infinitely and and always new files to it.
|
||||||
pub fn watchman(sources: Arc<Mutex<Vec<Media>>>) {
|
pub fn watchman(sources: Arc<Mutex<Vec<Media>>>) {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
let (tx, rx) = channel();
|
let (tx, rx) = channel();
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
io::{BufReader, Error, Read},
|
io::{BufReader, Error, Read},
|
||||||
path::Path,
|
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
sync::atomic::Ordering,
|
sync::atomic::Ordering,
|
||||||
thread,
|
thread,
|
||||||
@ -9,48 +8,12 @@ use std::{
|
|||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
|
|
||||||
|
use crate::filter::ingest_filter::filter_cmd;
|
||||||
use crate::utils::{stderr_reader, GlobalConfig, Ingest, ProcessControl};
|
use crate::utils::{stderr_reader, GlobalConfig, Ingest, ProcessControl};
|
||||||
|
|
||||||
fn overlay(config: &GlobalConfig) -> String {
|
/// ffmpeg Ingest Server
|
||||||
let mut logo_chain = String::new();
|
///
|
||||||
|
/// Start ffmpeg in listen mode, and wait for input.
|
||||||
if config.processing.add_logo && Path::new(&config.processing.logo).is_file() {
|
|
||||||
let opacity = format!(
|
|
||||||
"format=rgba,colorchannelmixer=aa={}",
|
|
||||||
config.processing.logo_opacity
|
|
||||||
);
|
|
||||||
let logo_loop = "loop=loop=-1:size=1:start=0";
|
|
||||||
logo_chain = format!("[v];movie={},{logo_loop},{opacity}", config.processing.logo);
|
|
||||||
|
|
||||||
logo_chain
|
|
||||||
.push_str(format!("[l];[v][l]{}:shortest=1", config.processing.logo_filter).as_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
logo_chain
|
|
||||||
}
|
|
||||||
|
|
||||||
fn audio_filter(config: &GlobalConfig) -> String {
|
|
||||||
let mut audio_chain = ";[0:a]afade=in:st=0:d=0.5".to_string();
|
|
||||||
|
|
||||||
if config.processing.add_loudnorm {
|
|
||||||
audio_chain.push_str(
|
|
||||||
format!(
|
|
||||||
",loudnorm=I={}:TP={}:LRA={}",
|
|
||||||
config.processing.loud_i, config.processing.loud_tp, config.processing.loud_lra
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.processing.volume != 1.0 {
|
|
||||||
audio_chain.push_str(format!(",volume={}", config.processing.volume).as_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
audio_chain.push_str("[aout1]");
|
|
||||||
|
|
||||||
audio_chain
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ingest_server(
|
pub fn ingest_server(
|
||||||
log_format: String,
|
log_format: String,
|
||||||
ingest_sender: Sender<(usize, [u8; 65088])>,
|
ingest_sender: Sender<(usize, [u8; 65088])>,
|
||||||
@ -58,32 +21,14 @@ pub fn ingest_server(
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
let mut buffer: [u8; 65088] = [0; 65088];
|
let mut buffer: [u8; 65088] = [0; 65088];
|
||||||
let mut filter = format!(
|
let filter_list = filter_cmd();
|
||||||
"[0:v]fps={},scale={}:{},setdar=dar={},fade=in:st=0:d=0.5",
|
|
||||||
config.processing.fps,
|
|
||||||
config.processing.width,
|
|
||||||
config.processing.height,
|
|
||||||
config.processing.aspect
|
|
||||||
);
|
|
||||||
|
|
||||||
filter.push_str(&overlay(&config));
|
|
||||||
filter.push_str("[vout1]");
|
|
||||||
filter.push_str(audio_filter(&config).as_str());
|
|
||||||
let mut filter_list = vec![
|
|
||||||
"-filter_complex",
|
|
||||||
&filter,
|
|
||||||
"-map",
|
|
||||||
"[vout1]",
|
|
||||||
"-map",
|
|
||||||
"[aout1]",
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut server_cmd = vec!["-hide_banner", "-nostats", "-v", log_format.as_str()];
|
let mut server_cmd = vec!["-hide_banner", "-nostats", "-v", log_format.as_str()];
|
||||||
let stream_input = config.ingest.input_cmd.clone().unwrap();
|
let stream_input = config.ingest.input_cmd.clone().unwrap();
|
||||||
let stream_settings = config.processing.settings.clone().unwrap();
|
let stream_settings = config.processing.settings.clone().unwrap();
|
||||||
|
|
||||||
server_cmd.append(&mut stream_input.iter().map(String::as_str).collect());
|
server_cmd.append(&mut stream_input.iter().map(String::as_str).collect());
|
||||||
server_cmd.append(&mut filter_list);
|
server_cmd.append(&mut filter_list.iter().map(String::as_str).collect());
|
||||||
server_cmd.append(&mut stream_settings.iter().map(String::as_str).collect());
|
server_cmd.append(&mut stream_settings.iter().map(String::as_str).collect());
|
||||||
|
|
||||||
let mut is_running;
|
let mut is_running;
|
||||||
@ -116,7 +61,6 @@ pub fn ingest_server(
|
|||||||
let error_reader_thread = thread::spawn(move || stderr_reader(server_err, "Server"));
|
let error_reader_thread = thread::spawn(move || stderr_reader(server_err, "Server"));
|
||||||
|
|
||||||
*proc_control.server_term.lock().unwrap() = Some(server_proc);
|
*proc_control.server_term.lock().unwrap() = Some(server_proc);
|
||||||
|
|
||||||
is_running = false;
|
is_running = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@ -146,7 +90,9 @@ pub fn ingest_server(
|
|||||||
}
|
}
|
||||||
|
|
||||||
drop(ingest_reader);
|
drop(ingest_reader);
|
||||||
proc_control.server_is_running.store(false, Ordering::SeqCst);
|
proc_control
|
||||||
|
.server_is_running
|
||||||
|
.store(false, Ordering::SeqCst);
|
||||||
|
|
||||||
if let Err(e) = proc_control.wait(Ingest) {
|
if let Err(e) = proc_control.wait(Ingest) {
|
||||||
error!("{e}")
|
error!("{e}")
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
use std::{
|
use std::{
|
||||||
process,
|
process,
|
||||||
sync::{Arc, Mutex, atomic::{AtomicBool, AtomicUsize}},
|
sync::{
|
||||||
|
atomic::{AtomicBool, AtomicUsize},
|
||||||
|
Arc, Mutex,
|
||||||
|
},
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -12,10 +15,11 @@ pub mod folder;
|
|||||||
pub mod ingest;
|
pub mod ingest;
|
||||||
pub mod playlist;
|
pub mod playlist;
|
||||||
|
|
||||||
pub use folder::{watchman, Source};
|
pub use folder::{watchman, FolderSource};
|
||||||
pub use ingest::ingest_server;
|
pub use ingest::ingest_server;
|
||||||
pub use playlist::CurrentProgram;
|
pub use playlist::CurrentProgram;
|
||||||
|
|
||||||
|
/// Create a source iterator from playlist, or from folder.
|
||||||
pub fn source_generator(
|
pub fn source_generator(
|
||||||
config: GlobalConfig,
|
config: GlobalConfig,
|
||||||
current_list: Arc<Mutex<Vec<Media>>>,
|
current_list: Arc<Mutex<Vec<Media>>>,
|
||||||
@ -23,31 +27,31 @@ pub fn source_generator(
|
|||||||
playout_stat: PlayoutStatus,
|
playout_stat: PlayoutStatus,
|
||||||
is_terminated: Arc<AtomicBool>,
|
is_terminated: Arc<AtomicBool>,
|
||||||
) -> Box<dyn Iterator<Item = Media>> {
|
) -> Box<dyn Iterator<Item = Media>> {
|
||||||
let get_source = match config.processing.clone().mode.as_str() {
|
let get_source = match config.processing.mode.as_str() {
|
||||||
"folder" => {
|
"folder" => {
|
||||||
info!("Playout in folder mode");
|
info!("Playout in folder mode");
|
||||||
debug!("Monitor folder: <b><magenta>{}</></b>", &config.storage.path);
|
debug!(
|
||||||
|
"Monitor folder: <b><magenta>{}</></b>",
|
||||||
|
&config.storage.path
|
||||||
|
);
|
||||||
|
|
||||||
let folder_source = Source::new(current_list, index);
|
let folder_source = FolderSource::new(current_list, index);
|
||||||
let node_clone = folder_source.nodes.clone();
|
let node_clone = folder_source.nodes.clone();
|
||||||
|
|
||||||
|
// Spawn a thread to monitor folder for file changes.
|
||||||
thread::spawn(move || watchman(node_clone));
|
thread::spawn(move || watchman(node_clone));
|
||||||
|
|
||||||
Box::new(folder_source) as Box<dyn Iterator<Item = Media>>
|
Box::new(folder_source) as Box<dyn Iterator<Item = Media>>
|
||||||
}
|
}
|
||||||
"playlist" => {
|
"playlist" => {
|
||||||
info!("Playout in playlist mode");
|
info!("Playout in playlist mode");
|
||||||
let program = CurrentProgram::new(
|
let program = CurrentProgram::new(playout_stat, is_terminated, current_list, index);
|
||||||
playout_stat,
|
|
||||||
is_terminated.clone(),
|
|
||||||
current_list,
|
|
||||||
index,
|
|
||||||
);
|
|
||||||
|
|
||||||
Box::new(program) as Box<dyn Iterator<Item = Media>>
|
Box::new(program) as Box<dyn Iterator<Item = Media>>
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
error!("Process Mode not exists!");
|
error!("Process Mode not exists!");
|
||||||
process::exit(0x0100);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
path::Path,
|
path::Path,
|
||||||
sync::{atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, Mutex},
|
sync::{
|
||||||
|
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||||
|
Arc, Mutex,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@ -12,6 +15,9 @@ use crate::utils::{
|
|||||||
seek_and_length, GlobalConfig, Media, PlayoutStatus, DUMMY_LEN,
|
seek_and_length, GlobalConfig, Media, PlayoutStatus, DUMMY_LEN,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Struct for current playlist.
|
||||||
|
///
|
||||||
|
/// Here we prepare the init clip and build a iterator where we pull our clips.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CurrentProgram {
|
pub struct CurrentProgram {
|
||||||
config: GlobalConfig,
|
config: GlobalConfig,
|
||||||
@ -63,6 +69,7 @@ impl CurrentProgram {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if playlist file got updated, and when yes we reload it and setup everything in place.
|
||||||
fn check_update(&mut self, seek: bool) {
|
fn check_update(&mut self, seek: bool) {
|
||||||
if self.json_path.is_none() {
|
if self.json_path.is_none() {
|
||||||
let json = read_json(None, self.is_terminated.clone(), seek, 0.0);
|
let json = read_json(None, self.is_terminated.clone(), seek, 0.0);
|
||||||
@ -115,15 +122,16 @@ impl CurrentProgram {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if day is past and it is time for a new playlist.
|
||||||
fn check_for_next_playlist(&mut self) {
|
fn check_for_next_playlist(&mut self) {
|
||||||
let current_time = get_sec();
|
let current_time = get_sec();
|
||||||
let start_sec = self.config.playlist.start_sec.unwrap();
|
let start_sec = self.config.playlist.start_sec.unwrap();
|
||||||
let target_length = self.config.playlist.length_sec.unwrap();
|
let target_length = self.config.playlist.length_sec.unwrap();
|
||||||
let (delta, total_delta) = get_delta(¤t_time);
|
let (delta, total_delta) = get_delta(¤t_time);
|
||||||
let mut duration = self.current_node.out.clone();
|
let mut duration = self.current_node.out;
|
||||||
|
|
||||||
if self.current_node.duration > self.current_node.out {
|
if self.current_node.duration > self.current_node.out {
|
||||||
duration = self.current_node.duration.clone()
|
duration = self.current_node.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
let next_start = self.current_node.begin.unwrap() - start_sec + duration + delta;
|
let next_start = self.current_node.begin.unwrap() - start_sec + duration + delta;
|
||||||
@ -158,32 +166,27 @@ impl CurrentProgram {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if last and/or next clip is a advertisement.
|
||||||
fn last_next_ad(&mut self) {
|
fn last_next_ad(&mut self) {
|
||||||
let index = self.index.load(Ordering::SeqCst);
|
let index = self.index.load(Ordering::SeqCst);
|
||||||
let current_list = self.nodes.lock().unwrap();
|
let current_list = self.nodes.lock().unwrap();
|
||||||
|
|
||||||
if index + 1 < current_list.len()
|
if index + 1 < current_list.len()
|
||||||
&& ¤t_list[index + 1]
|
&& ¤t_list[index + 1].category.clone().unwrap_or_default() == "advertisement"
|
||||||
.category
|
|
||||||
.clone()
|
|
||||||
.unwrap_or(String::new())
|
|
||||||
== "advertisement"
|
|
||||||
{
|
{
|
||||||
self.current_node.next_ad = Some(true);
|
self.current_node.next_ad = Some(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if index > 0
|
if index > 0
|
||||||
&& index < current_list.len()
|
&& index < current_list.len()
|
||||||
&& ¤t_list[index - 1]
|
&& ¤t_list[index - 1].category.clone().unwrap_or_default() == "advertisement"
|
||||||
.category
|
|
||||||
.clone()
|
|
||||||
.unwrap_or(String::new())
|
|
||||||
== "advertisement"
|
|
||||||
{
|
{
|
||||||
self.current_node.last_ad = Some(true);
|
self.current_node.last_ad = Some(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current time and when we are before start time,
|
||||||
|
// we add full seconds of a day to it.
|
||||||
fn get_current_time(&mut self) -> f64 {
|
fn get_current_time(&mut self) -> f64 {
|
||||||
let mut time_sec = get_sec();
|
let mut time_sec = get_sec();
|
||||||
|
|
||||||
@ -194,6 +197,7 @@ impl CurrentProgram {
|
|||||||
time_sec
|
time_sec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On init or reload we need to seek for the current clip.
|
||||||
fn get_current_clip(&mut self) {
|
fn get_current_clip(&mut self) {
|
||||||
let mut time_sec = self.get_current_time();
|
let mut time_sec = self.get_current_time();
|
||||||
|
|
||||||
@ -216,6 +220,7 @@ impl CurrentProgram {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare init clip.
|
||||||
fn init_clip(&mut self) {
|
fn init_clip(&mut self) {
|
||||||
self.get_current_clip();
|
self.get_current_clip();
|
||||||
|
|
||||||
@ -232,6 +237,7 @@ impl CurrentProgram {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the playlist iterator
|
||||||
impl Iterator for CurrentProgram {
|
impl Iterator for CurrentProgram {
|
||||||
type Item = Media;
|
type Item = Media;
|
||||||
|
|
||||||
@ -245,7 +251,7 @@ impl Iterator for CurrentProgram {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.playout_stat.list_init.load(Ordering::SeqCst) {
|
if self.playout_stat.list_init.load(Ordering::SeqCst) {
|
||||||
// on init load playlist, could be not long enough,
|
// On init load, playlist could be not long enough,
|
||||||
// so we check if we can take the next playlist already,
|
// so we check if we can take the next playlist already,
|
||||||
// or we fill the gap with a dummy.
|
// or we fill the gap with a dummy.
|
||||||
let list_length = self.nodes.lock().unwrap().len();
|
let list_length = self.nodes.lock().unwrap().len();
|
||||||
@ -281,7 +287,8 @@ impl Iterator for CurrentProgram {
|
|||||||
|
|
||||||
self.current_node = gen_source(media);
|
self.current_node = gen_source(media);
|
||||||
self.nodes.lock().unwrap().push(self.current_node.clone());
|
self.nodes.lock().unwrap().push(self.current_node.clone());
|
||||||
self.index.store(self.nodes.lock().unwrap().len(), Ordering::SeqCst);
|
self.index
|
||||||
|
.store(self.nodes.lock().unwrap().len(), Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,7 +321,7 @@ impl Iterator for CurrentProgram {
|
|||||||
Some(self.current_node.clone())
|
Some(self.current_node.clone())
|
||||||
} else {
|
} else {
|
||||||
let last_playlist = self.json_path.clone();
|
let last_playlist = self.json_path.clone();
|
||||||
let last_ad = self.current_node.last_ad.clone();
|
let last_ad = self.current_node.last_ad;
|
||||||
self.check_for_next_playlist();
|
self.check_for_next_playlist();
|
||||||
let (_, total_delta) = get_delta(&self.config.playlist.start_sec.unwrap());
|
let (_, total_delta) = get_delta(&self.config.playlist.start_sec.unwrap());
|
||||||
|
|
||||||
@ -357,22 +364,22 @@ impl Iterator for CurrentProgram {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepare input clip:
|
||||||
|
///
|
||||||
|
/// - check begin and length from clip
|
||||||
|
/// - return clip only if we are in 24 hours time range
|
||||||
fn timed_source(
|
fn timed_source(
|
||||||
node: Media,
|
node: Media,
|
||||||
config: &GlobalConfig,
|
config: &GlobalConfig,
|
||||||
last: bool,
|
last: bool,
|
||||||
playout_stat: &PlayoutStatus,
|
playout_stat: &PlayoutStatus,
|
||||||
) -> Media {
|
) -> Media {
|
||||||
// prepare input clip
|
|
||||||
// check begin and length from clip
|
|
||||||
// return clip only if we are in 24 hours time range
|
|
||||||
|
|
||||||
let (delta, total_delta) = get_delta(&node.begin.unwrap());
|
let (delta, total_delta) = get_delta(&node.begin.unwrap());
|
||||||
let mut shifted_delta = delta;
|
let mut shifted_delta = delta;
|
||||||
let mut new_node = node.clone();
|
let mut new_node = node.clone();
|
||||||
new_node.process = Some(false);
|
new_node.process = Some(false);
|
||||||
|
|
||||||
if config.playlist.length.contains(":") {
|
if config.playlist.length.contains(':') {
|
||||||
let time_shift = playout_stat.time_shift.lock().unwrap();
|
let time_shift = playout_stat.time_shift.lock().unwrap();
|
||||||
|
|
||||||
if *playout_stat.current_date.lock().unwrap() == *playout_stat.date.lock().unwrap()
|
if *playout_stat.current_date.lock().unwrap() == *playout_stat.date.lock().unwrap()
|
||||||
@ -398,7 +405,7 @@ fn timed_source(
|
|||||||
|
|
||||||
if (total_delta > node.out - node.seek && !last)
|
if (total_delta > node.out - node.seek && !last)
|
||||||
|| node.index.unwrap() < 2
|
|| node.index.unwrap() < 2
|
||||||
|| !config.playlist.length.contains(":")
|
|| !config.playlist.length.contains(':')
|
||||||
{
|
{
|
||||||
// when we are in the 24 hour range, get the clip
|
// when we are in the 24 hour range, get the clip
|
||||||
new_node = gen_source(node);
|
new_node = gen_source(node);
|
||||||
@ -412,6 +419,7 @@ fn timed_source(
|
|||||||
new_node
|
new_node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate the source CMD, or when clip not exist, get a dummy.
|
||||||
fn gen_source(mut node: Media) -> Media {
|
fn gen_source(mut node: Media) -> Media {
|
||||||
if Path::new(&node.source).is_file() {
|
if Path::new(&node.source).is_file() {
|
||||||
node.add_probe();
|
node.add_probe();
|
||||||
@ -440,10 +448,9 @@ fn gen_source(mut node: Media) -> Media {
|
|||||||
node
|
node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle init clip, but this clip can be the last one in playlist,
|
||||||
|
/// this we have to figure out and calculate the right length.
|
||||||
fn handle_list_init(mut node: Media) -> Media {
|
fn handle_list_init(mut node: Media) -> Media {
|
||||||
// handle init clip, but this clip can be the last one in playlist,
|
|
||||||
// this we have to figure out and calculate the right length
|
|
||||||
|
|
||||||
let (_, total_delta) = get_delta(&node.begin.unwrap());
|
let (_, total_delta) = get_delta(&node.begin.unwrap());
|
||||||
let mut out = node.out;
|
let mut out = node.out;
|
||||||
|
|
||||||
@ -452,16 +459,13 @@ fn handle_list_init(mut node: Media) -> Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
node.out = out;
|
node.out = out;
|
||||||
|
gen_source(node)
|
||||||
let new_node = gen_source(node);
|
|
||||||
new_node
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// when we come to last clip in playlist,
|
||||||
|
/// or when we reached total playtime,
|
||||||
|
/// we end up here
|
||||||
fn handle_list_end(mut node: Media, total_delta: f64) -> Media {
|
fn handle_list_end(mut node: Media, total_delta: f64) -> Media {
|
||||||
// when we come to last clip in playlist,
|
|
||||||
// or when we reached total playtime,
|
|
||||||
// we end up here
|
|
||||||
|
|
||||||
debug!("Playlist end");
|
debug!("Playlist end");
|
||||||
|
|
||||||
let mut out = if node.seek > 0.0 {
|
let mut out = if node.seek > 0.0 {
|
||||||
|
43
src/main.rs
43
src/main.rs
@ -2,11 +2,10 @@ extern crate log;
|
|||||||
extern crate simplelog;
|
extern crate simplelog;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
{fs, fs::File},
|
fs::{self, File},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process::exit,
|
process::exit,
|
||||||
thread,
|
thread,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -32,26 +31,26 @@ struct StatusData {
|
|||||||
date: String,
|
date: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
/// Here we create a status file in temp folder.
|
||||||
init_config();
|
/// We need this for reading/saving program status.
|
||||||
let config = GlobalConfig::global();
|
/// For example when we skip a playing file,
|
||||||
let play_control = PlayerControl::new();
|
/// we save the time difference, so we stay in sync.
|
||||||
let playout_stat = PlayoutStatus::new();
|
///
|
||||||
let proc_control = ProcessControl::new();
|
/// When file not exists we create it, and when it exists we get its values.
|
||||||
|
fn status_file(stat_file: &str, playout_stat: &PlayoutStatus) {
|
||||||
if !PathBuf::from(config.general.stat_file.clone()).exists() {
|
if !PathBuf::from(stat_file).exists() {
|
||||||
let data = json!({
|
let data = json!({
|
||||||
"time_shift": 0.0,
|
"time_shift": 0.0,
|
||||||
"date": String::new(),
|
"date": String::new(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let json: String = serde_json::to_string(&data).expect("Serialize status data failed");
|
let json: String = serde_json::to_string(&data).expect("Serialize status data failed");
|
||||||
fs::write(config.general.stat_file.clone(), &json).expect("Unable to write file");
|
fs::write(stat_file, &json).expect("Unable to write file");
|
||||||
} else {
|
} else {
|
||||||
let stat_file = File::options()
|
let stat_file = File::options()
|
||||||
.read(true)
|
.read(true)
|
||||||
.write(false)
|
.write(false)
|
||||||
.open(&config.general.stat_file)
|
.open(&stat_file)
|
||||||
.expect("Could not open status file");
|
.expect("Could not open status file");
|
||||||
|
|
||||||
let data: StatusData =
|
let data: StatusData =
|
||||||
@ -60,13 +59,24 @@ fn main() {
|
|||||||
*playout_stat.time_shift.lock().unwrap() = data.time_shift;
|
*playout_stat.time_shift.lock().unwrap() = data.time_shift;
|
||||||
*playout_stat.date.lock().unwrap() = data.date;
|
*playout_stat.date.lock().unwrap() = data.date;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Init the config, set process controller, create logging.
|
||||||
|
init_config();
|
||||||
|
let config = GlobalConfig::global();
|
||||||
|
let play_control = PlayerControl::new();
|
||||||
|
let playout_stat = PlayoutStatus::new();
|
||||||
|
let proc_control = ProcessControl::new();
|
||||||
|
|
||||||
let logging = init_logging();
|
let logging = init_logging();
|
||||||
CombinedLogger::init(logging).unwrap();
|
CombinedLogger::init(logging).unwrap();
|
||||||
|
|
||||||
validate_ffmpeg();
|
validate_ffmpeg();
|
||||||
|
status_file(&config.general.stat_file, &playout_stat);
|
||||||
|
|
||||||
if let Some(range) = config.general.generate.clone() {
|
if let Some(range) = config.general.generate.clone() {
|
||||||
|
// run a simple playlist generator and save them to disk
|
||||||
generate_playlist(range);
|
generate_playlist(range);
|
||||||
|
|
||||||
exit(0);
|
exit(0);
|
||||||
@ -77,16 +87,15 @@ fn main() {
|
|||||||
let proc_ctl = proc_control.clone();
|
let proc_ctl = proc_control.clone();
|
||||||
|
|
||||||
if config.rpc_server.enable {
|
if config.rpc_server.enable {
|
||||||
thread::spawn( move || json_rpc_server(
|
// If RPC server is enable we also fire up a JSON RPC server.
|
||||||
play_ctl,
|
thread::spawn(move || json_rpc_server(play_ctl, play_stat, proc_ctl));
|
||||||
play_stat,
|
|
||||||
proc_ctl,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if &config.out.mode.to_lowercase() == "hls" {
|
if &config.out.mode.to_lowercase() == "hls" {
|
||||||
|
// write files/playlist to HLS m3u8 playlist
|
||||||
write_hls(play_control, playout_stat, proc_control);
|
write_hls(play_control, playout_stat, proc_control);
|
||||||
} else {
|
} else {
|
||||||
|
// play on desktop or stream to a remote target
|
||||||
player(play_control, playout_stat, proc_control);
|
player(play_control, playout_stat, proc_control);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,9 @@ use simplelog::*;
|
|||||||
use crate::filter::v_drawtext;
|
use crate::filter::v_drawtext;
|
||||||
use crate::utils::{GlobalConfig, Media};
|
use crate::utils::{GlobalConfig, Media};
|
||||||
|
|
||||||
|
/// Desktop Output
|
||||||
|
///
|
||||||
|
/// Instead of streaming, we run a ffplay instance and play on desktop.
|
||||||
pub fn output(log_format: &str) -> process::Child {
|
pub fn output(log_format: &str) -> process::Child {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
|
|
||||||
|
@ -18,26 +18,129 @@ out:
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::BufReader,
|
io::{BufRead, BufReader, Error},
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
thread,
|
sync::atomic::Ordering,
|
||||||
|
thread::{self, sleep},
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
|
|
||||||
|
use crate::filter::ingest_filter::filter_cmd;
|
||||||
use crate::input::source_generator;
|
use crate::input::source_generator;
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
sec_to_time, stderr_reader, GlobalConfig, PlayerControl, PlayoutStatus, ProcessControl,
|
sec_to_time, stderr_reader, Decoder, GlobalConfig, Ingest, PlayerControl, PlayoutStatus,
|
||||||
|
ProcessControl,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn format_line(line: String, level: &str) -> String {
|
||||||
|
line.replace(&format!("[{level: >5}] "), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ingest Server for HLS
|
||||||
|
fn ingest_to_hls_server(
|
||||||
|
playout_stat: PlayoutStatus,
|
||||||
|
mut proc_control: ProcessControl,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let config = GlobalConfig::global();
|
||||||
|
let dec_settings = config.out.clone().output_cmd.unwrap();
|
||||||
|
let playlist_init = playout_stat.list_init;
|
||||||
|
let filter_list = filter_cmd();
|
||||||
|
|
||||||
|
let mut server_cmd = vec!["-hide_banner", "-nostats", "-v", "level+info"];
|
||||||
|
let stream_input = config.ingest.input_cmd.clone().unwrap();
|
||||||
|
|
||||||
|
server_cmd.append(&mut stream_input.iter().map(String::as_str).collect());
|
||||||
|
server_cmd.append(&mut filter_list.iter().map(String::as_str).collect());
|
||||||
|
server_cmd.append(&mut dec_settings.iter().map(String::as_str).collect());
|
||||||
|
|
||||||
|
let mut is_running;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Start ingest server, listening on: <b><magenta>{}</></b>",
|
||||||
|
stream_input.last().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Server CMD: <bright-blue>\"ffmpeg {}\"</>",
|
||||||
|
server_cmd.join(" ")
|
||||||
|
);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut server_proc = match Command::new("ffmpeg")
|
||||||
|
.args(server_cmd.clone())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
Err(e) => {
|
||||||
|
error!("couldn't spawn ingest server: {e}");
|
||||||
|
panic!("couldn't spawn ingest server: {e}")
|
||||||
|
}
|
||||||
|
Ok(proc) => proc,
|
||||||
|
};
|
||||||
|
|
||||||
|
let server_err = BufReader::new(server_proc.stderr.take().unwrap());
|
||||||
|
*proc_control.server_term.lock().unwrap() = Some(server_proc);
|
||||||
|
is_running = false;
|
||||||
|
|
||||||
|
for line in server_err.lines() {
|
||||||
|
let line = line?;
|
||||||
|
|
||||||
|
if !is_running {
|
||||||
|
proc_control.server_is_running.store(true, Ordering::SeqCst);
|
||||||
|
playlist_init.store(true, Ordering::SeqCst);
|
||||||
|
is_running = true;
|
||||||
|
|
||||||
|
info!("Switch from {} to live ingest", config.processing.mode);
|
||||||
|
|
||||||
|
if let Err(e) = proc_control.kill(Decoder) {
|
||||||
|
error!("{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.contains("[error]")
|
||||||
|
&& !line.contains("Input/output error")
|
||||||
|
&& !line.contains("Broken pipe")
|
||||||
|
{
|
||||||
|
error!(
|
||||||
|
"<bright black>[server]</> {}",
|
||||||
|
format_line(line.clone(), "error")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Switch from live ingest to {}", config.processing.mode);
|
||||||
|
|
||||||
|
proc_control
|
||||||
|
.server_is_running
|
||||||
|
.store(false, Ordering::SeqCst);
|
||||||
|
|
||||||
|
if let Err(e) = proc_control.wait(Ingest) {
|
||||||
|
error!("{e}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if proc_control.is_terminated.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HLS Writer
|
||||||
|
///
|
||||||
|
/// Write with single ffmpeg instance directly to a HLS playlist.
|
||||||
pub fn write_hls(
|
pub fn write_hls(
|
||||||
play_control: PlayerControl,
|
play_control: PlayerControl,
|
||||||
playout_stat: PlayoutStatus,
|
playout_stat: PlayoutStatus,
|
||||||
proc_control: ProcessControl,
|
mut proc_control: ProcessControl,
|
||||||
) {
|
) {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
let dec_settings = config.out.clone().output_cmd.unwrap();
|
let dec_settings = config.out.clone().output_cmd.unwrap();
|
||||||
let ff_log_format = format!("level+{}", config.logging.ffmpeg_level.to_lowercase());
|
let ff_log_format = format!("level+{}", config.logging.ffmpeg_level.to_lowercase());
|
||||||
|
let play_stat = playout_stat.clone();
|
||||||
|
let proc_control_c = proc_control.clone();
|
||||||
|
|
||||||
let get_source = source_generator(
|
let get_source = source_generator(
|
||||||
config.clone(),
|
config.clone(),
|
||||||
@ -47,6 +150,11 @@ pub fn write_hls(
|
|||||||
proc_control.is_terminated.clone(),
|
proc_control.is_terminated.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// spawn a thread for ffmpeg ingest server and create a channel for package sending
|
||||||
|
if config.ingest.enable {
|
||||||
|
thread::spawn(move || ingest_to_hls_server(play_stat, proc_control_c));
|
||||||
|
}
|
||||||
|
|
||||||
for node in get_source {
|
for node in get_source {
|
||||||
*play_control.current_media.lock().unwrap() = Some(node.clone());
|
*play_control.current_media.lock().unwrap() = Some(node.clone());
|
||||||
|
|
||||||
@ -93,14 +201,18 @@ pub fn write_hls(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let dec_err = BufReader::new(dec_proc.stderr.take().unwrap());
|
let dec_err = BufReader::new(dec_proc.stderr.take().unwrap());
|
||||||
let error_decoder_thread = thread::spawn(move || stderr_reader(dec_err, "Writer"));
|
*proc_control.decoder_term.lock().unwrap() = Some(dec_proc);
|
||||||
|
|
||||||
if let Err(e) = dec_proc.wait() {
|
if let Err(e) = stderr_reader(dec_err, "Writer") {
|
||||||
error!("Writer: {e}")
|
error!("{e:?}")
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = error_decoder_thread.join() {
|
if let Err(e) = proc_control.wait(Decoder) {
|
||||||
error!("{e:?}");
|
error!("{e}");
|
||||||
};
|
}
|
||||||
|
|
||||||
|
while proc_control.server_is_running.load(Ordering::SeqCst) {
|
||||||
|
sleep(Duration::from_secs(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,15 @@ use crate::utils::{
|
|||||||
ProcessControl,
|
ProcessControl,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Player
|
||||||
|
///
|
||||||
|
/// Here we create the input file loop, from playlist, or folder source.
|
||||||
|
/// Then we read the stdout from the reader ffmpeg instance
|
||||||
|
/// and write it to the stdin from the streamer ffmpeg instance.
|
||||||
|
/// If it is configured we also fire up a ffmpeg ingest server instance,
|
||||||
|
/// for getting live feeds.
|
||||||
|
/// When a live ingest arrive, it stops the current playing and switch to the live source.
|
||||||
|
/// When ingest stops, it switch back to playlist/folder mode.
|
||||||
pub fn player(
|
pub fn player(
|
||||||
play_control: PlayerControl,
|
play_control: PlayerControl,
|
||||||
playout_stat: PlayoutStatus,
|
playout_stat: PlayoutStatus,
|
||||||
@ -33,6 +42,7 @@ pub fn player(
|
|||||||
let mut live_on = false;
|
let mut live_on = false;
|
||||||
let playlist_init = playout_stat.list_init.clone();
|
let playlist_init = playout_stat.list_init.clone();
|
||||||
|
|
||||||
|
// get source iterator
|
||||||
let get_source = source_generator(
|
let get_source = source_generator(
|
||||||
config.clone(),
|
config.clone(),
|
||||||
play_control.current_list.clone(),
|
play_control.current_list.clone(),
|
||||||
@ -41,6 +51,7 @@ pub fn player(
|
|||||||
proc_control.is_terminated.clone(),
|
proc_control.is_terminated.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// get ffmpeg output instance
|
||||||
let mut enc_proc = match config.out.mode.as_str() {
|
let mut enc_proc = match config.out.mode.as_str() {
|
||||||
"desktop" => desktop::output(&ff_log_format),
|
"desktop" => desktop::output(&ff_log_format),
|
||||||
"stream" => stream::output(&ff_log_format),
|
"stream" => stream::output(&ff_log_format),
|
||||||
@ -49,16 +60,20 @@ pub fn player(
|
|||||||
|
|
||||||
let mut enc_writer = BufWriter::new(enc_proc.stdin.take().unwrap());
|
let mut enc_writer = BufWriter::new(enc_proc.stdin.take().unwrap());
|
||||||
let enc_err = BufReader::new(enc_proc.stderr.take().unwrap());
|
let enc_err = BufReader::new(enc_proc.stderr.take().unwrap());
|
||||||
|
|
||||||
|
// spawn a thread to log ffmpeg output error messages
|
||||||
let error_encoder_thread = thread::spawn(move || stderr_reader(enc_err, "Encoder"));
|
let error_encoder_thread = thread::spawn(move || stderr_reader(enc_err, "Encoder"));
|
||||||
|
|
||||||
*proc_control.decoder_term.lock().unwrap() = Some(enc_proc);
|
*proc_control.decoder_term.lock().unwrap() = Some(enc_proc);
|
||||||
|
|
||||||
let (ingest_sender, ingest_receiver) = bounded(96);
|
|
||||||
|
|
||||||
let ff_log_format_c = ff_log_format.clone();
|
let ff_log_format_c = ff_log_format.clone();
|
||||||
let proc_control_c = proc_control.clone();
|
let proc_control_c = proc_control.clone();
|
||||||
|
let mut ingest_receiver = None;
|
||||||
|
|
||||||
|
// spawn a thread for ffmpeg ingest server and create a channel for package sending
|
||||||
if config.ingest.enable {
|
if config.ingest.enable {
|
||||||
|
let (ingest_sender, rx) = bounded(96);
|
||||||
|
ingest_receiver = Some(rx);
|
||||||
thread::spawn(move || ingest_server(ff_log_format_c, ingest_sender, proc_control_c));
|
thread::spawn(move || ingest_server(ff_log_format_c, ingest_sender, proc_control_c));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +110,7 @@ pub fn player(
|
|||||||
dec_cmd.join(" ")
|
dec_cmd.join(" ")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// create ffmpeg decoder instance, for reading the input files
|
||||||
let mut dec_proc = match Command::new("ffmpeg")
|
let mut dec_proc = match Command::new("ffmpeg")
|
||||||
.args(dec_cmd)
|
.args(dec_cmd)
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
@ -115,6 +131,7 @@ pub fn player(
|
|||||||
*proc_control.decoder_term.lock().unwrap() = Some(dec_proc);
|
*proc_control.decoder_term.lock().unwrap() = Some(dec_proc);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
// when server is running, read from channel
|
||||||
if proc_control.server_is_running.load(Ordering::SeqCst) {
|
if proc_control.server_is_running.load(Ordering::SeqCst) {
|
||||||
if !live_on {
|
if !live_on {
|
||||||
info!("Switch from {} to live ingest", config.processing.mode);
|
info!("Switch from {} to live ingest", config.processing.mode);
|
||||||
@ -131,13 +148,14 @@ pub fn player(
|
|||||||
playlist_init.store(true, Ordering::SeqCst);
|
playlist_init.store(true, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
for rx in ingest_receiver.try_iter() {
|
for rx in ingest_receiver.as_ref().unwrap().try_iter() {
|
||||||
if let Err(e) = enc_writer.write(&rx.1[..rx.0]) {
|
if let Err(e) = enc_writer.write(&rx.1[..rx.0]) {
|
||||||
error!("Encoder write error: {:?}", e);
|
error!("Encoder write error: {:?}", e);
|
||||||
|
|
||||||
break 'source_iter;
|
break 'source_iter;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// read from decoder instance
|
||||||
} else {
|
} else {
|
||||||
if live_on {
|
if live_on {
|
||||||
info!("Switch from live ingest to {}", config.processing.mode);
|
info!("Switch from live ingest to {}", config.processing.mode);
|
||||||
|
@ -8,6 +8,9 @@ use simplelog::*;
|
|||||||
use crate::filter::v_drawtext;
|
use crate::filter::v_drawtext;
|
||||||
use crate::utils::{GlobalConfig, Media};
|
use crate::utils::{GlobalConfig, Media};
|
||||||
|
|
||||||
|
/// Streaming Output
|
||||||
|
///
|
||||||
|
/// Prepare the ffmpeg command for streaming output
|
||||||
pub fn output(log_format: &str) -> process::Child {
|
pub fn output(log_format: &str) -> process::Child {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
let mut enc_filter: Vec<String> = vec![];
|
let mut enc_filter: Vec<String> = vec![];
|
||||||
@ -51,7 +54,10 @@ pub fn output(log_format: &str) -> process::Child {
|
|||||||
enc_cmd.append(&mut preview);
|
enc_cmd.append(&mut preview);
|
||||||
enc_cmd.append(&mut output_cmd.iter().map(String::as_str).collect());
|
enc_cmd.append(&mut output_cmd.iter().map(String::as_str).collect());
|
||||||
|
|
||||||
debug!("Encoder CMD: <bright-blue>\"ffmpeg {}\"</>", enc_cmd.join(" "));
|
debug!(
|
||||||
|
"Encoder CMD: <bright-blue>\"ffmpeg {}\"</>",
|
||||||
|
enc_cmd.join(" ")
|
||||||
|
);
|
||||||
|
|
||||||
let enc_proc = match Command::new("ffmpeg")
|
let enc_proc = match Command::new("ffmpeg")
|
||||||
.args(enc_cmd)
|
.args(enc_cmd)
|
||||||
|
@ -13,6 +13,7 @@ use crate::utils::{
|
|||||||
PlayoutStatus, ProcessControl,
|
PlayoutStatus, ProcessControl,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// map media struct to json object
|
||||||
fn get_media_map(media: Media) -> Value {
|
fn get_media_map(media: Media) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"seek": media.seek,
|
"seek": media.seek,
|
||||||
@ -23,6 +24,7 @@ fn get_media_map(media: Media) -> Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// prepare json object for response
|
||||||
fn get_data_map(config: &GlobalConfig, media: Media) -> Map<String, Value> {
|
fn get_data_map(config: &GlobalConfig, media: Media) -> Map<String, Value> {
|
||||||
let mut data_map = Map::new();
|
let mut data_map = Map::new();
|
||||||
let begin = media.begin.unwrap_or(0.0);
|
let begin = media.begin.unwrap_or(0.0);
|
||||||
@ -45,6 +47,14 @@ fn get_data_map(config: &GlobalConfig, media: Media) -> Map<String, Value> {
|
|||||||
data_map
|
data_map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// JSON RPC Server
|
||||||
|
///
|
||||||
|
/// A simple rpc server for getting status information and controlling player:
|
||||||
|
///
|
||||||
|
/// - current clip information
|
||||||
|
/// - jump to next clip
|
||||||
|
/// - get last clip
|
||||||
|
/// - reset player state to original clip
|
||||||
pub fn json_rpc_server(
|
pub fn json_rpc_server(
|
||||||
play_control: PlayerControl,
|
play_control: PlayerControl,
|
||||||
playout_stat: PlayoutStatus,
|
playout_stat: PlayoutStatus,
|
||||||
@ -52,7 +62,6 @@ pub fn json_rpc_server(
|
|||||||
) {
|
) {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
let mut io = IoHandler::default();
|
let mut io = IoHandler::default();
|
||||||
let play = play_control.clone();
|
|
||||||
let proc = proc_control.clone();
|
let proc = proc_control.clone();
|
||||||
|
|
||||||
io.add_sync_method("player", move |params: Params| {
|
io.add_sync_method("player", move |params: Params| {
|
||||||
@ -61,10 +70,11 @@ pub fn json_rpc_server(
|
|||||||
let current_date = playout_stat.current_date.lock().unwrap().clone();
|
let current_date = playout_stat.current_date.lock().unwrap().clone();
|
||||||
let mut date = playout_stat.date.lock().unwrap();
|
let mut date = playout_stat.date.lock().unwrap();
|
||||||
|
|
||||||
|
// get next clip
|
||||||
if map.contains_key("control") && &map["control"] == "next" {
|
if map.contains_key("control") && &map["control"] == "next" {
|
||||||
let index = play.index.load(Ordering::SeqCst);
|
let index = play_control.index.load(Ordering::SeqCst);
|
||||||
|
|
||||||
if index < play.current_list.lock().unwrap().len() {
|
if index < play_control.current_list.lock().unwrap().len() {
|
||||||
if let Some(proc) = proc.decoder_term.lock().unwrap().as_mut() {
|
if let Some(proc) = proc.decoder_term.lock().unwrap().as_mut() {
|
||||||
if let Err(e) = proc.kill() {
|
if let Err(e) = proc.kill() {
|
||||||
error!("Decoder {e:?}")
|
error!("Decoder {e:?}")
|
||||||
@ -77,7 +87,7 @@ pub fn json_rpc_server(
|
|||||||
info!("Move to next clip");
|
info!("Move to next clip");
|
||||||
|
|
||||||
let mut data_map = Map::new();
|
let mut data_map = Map::new();
|
||||||
let mut media = play.current_list.lock().unwrap()[index].clone();
|
let mut media = play_control.current_list.lock().unwrap()[index].clone();
|
||||||
media.add_probe();
|
media.add_probe();
|
||||||
|
|
||||||
let (delta, _) = get_delta(&media.begin.unwrap_or(0.0));
|
let (delta, _) = get_delta(&media.begin.unwrap_or(0.0));
|
||||||
@ -98,10 +108,11 @@ pub fn json_rpc_server(
|
|||||||
return Ok(Value::String("Last clip can not be skipped".to_string()));
|
return Ok(Value::String("Last clip can not be skipped".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get last clip
|
||||||
if map.contains_key("control") && &map["control"] == "back" {
|
if map.contains_key("control") && &map["control"] == "back" {
|
||||||
let index = play.index.load(Ordering::SeqCst);
|
let index = play_control.index.load(Ordering::SeqCst);
|
||||||
|
|
||||||
if index > 1 && play.current_list.lock().unwrap().len() > 1 {
|
if index > 1 && play_control.current_list.lock().unwrap().len() > 1 {
|
||||||
if let Some(proc) = proc.decoder_term.lock().unwrap().as_mut() {
|
if let Some(proc) = proc.decoder_term.lock().unwrap().as_mut() {
|
||||||
if let Err(e) = proc.kill() {
|
if let Err(e) = proc.kill() {
|
||||||
error!("Decoder {e:?}")
|
error!("Decoder {e:?}")
|
||||||
@ -113,8 +124,9 @@ pub fn json_rpc_server(
|
|||||||
|
|
||||||
info!("Move to last clip");
|
info!("Move to last clip");
|
||||||
let mut data_map = Map::new();
|
let mut data_map = Map::new();
|
||||||
let mut media = play.current_list.lock().unwrap()[index - 2].clone();
|
let mut media =
|
||||||
play.index.fetch_sub(2, Ordering::SeqCst);
|
play_control.current_list.lock().unwrap()[index - 2].clone();
|
||||||
|
play_control.index.fetch_sub(2, Ordering::SeqCst);
|
||||||
media.add_probe();
|
media.add_probe();
|
||||||
|
|
||||||
let (delta, _) = get_delta(&media.begin.unwrap_or(0.0));
|
let (delta, _) = get_delta(&media.begin.unwrap_or(0.0));
|
||||||
@ -135,6 +147,7 @@ pub fn json_rpc_server(
|
|||||||
return Ok(Value::String("Clip index out of range".to_string()));
|
return Ok(Value::String("Clip index out of range".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reset player state
|
||||||
if map.contains_key("control") && &map["control"] == "reset" {
|
if map.contains_key("control") && &map["control"] == "reset" {
|
||||||
if let Some(proc) = proc.decoder_term.lock().unwrap().as_mut() {
|
if let Some(proc) = proc.decoder_term.lock().unwrap().as_mut() {
|
||||||
if let Err(e) = proc.kill() {
|
if let Err(e) = proc.kill() {
|
||||||
@ -161,19 +174,21 @@ pub fn json_rpc_server(
|
|||||||
return Ok(Value::String("Reset playout state failed".to_string()));
|
return Ok(Value::String("Reset playout state failed".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get infos about current clip
|
||||||
if map.contains_key("media") && &map["media"] == "current" {
|
if map.contains_key("media") && &map["media"] == "current" {
|
||||||
if let Some(media) = play.current_media.lock().unwrap().clone() {
|
if let Some(media) = play_control.current_media.lock().unwrap().clone() {
|
||||||
let data_map = get_data_map(config, media);
|
let data_map = get_data_map(config, media);
|
||||||
|
|
||||||
return Ok(Value::Object(data_map));
|
return Ok(Value::Object(data_map));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get infos about next clip
|
||||||
if map.contains_key("media") && &map["media"] == "next" {
|
if map.contains_key("media") && &map["media"] == "next" {
|
||||||
let index = play.index.load(Ordering::SeqCst);
|
let index = play_control.index.load(Ordering::SeqCst);
|
||||||
|
|
||||||
if index < play.current_list.lock().unwrap().len() {
|
if index < play_control.current_list.lock().unwrap().len() {
|
||||||
let media = play.current_list.lock().unwrap()[index].clone();
|
let media = play_control.current_list.lock().unwrap()[index].clone();
|
||||||
|
|
||||||
let data_map = get_data_map(config, media);
|
let data_map = get_data_map(config, media);
|
||||||
|
|
||||||
@ -183,11 +198,12 @@ pub fn json_rpc_server(
|
|||||||
return Ok(Value::String("There is no next clip".to_string()));
|
return Ok(Value::String("There is no next clip".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get infos about last clip
|
||||||
if map.contains_key("media") && &map["media"] == "last" {
|
if map.contains_key("media") && &map["media"] == "last" {
|
||||||
let index = play.index.load(Ordering::SeqCst);
|
let index = play_control.index.load(Ordering::SeqCst);
|
||||||
|
|
||||||
if index > 1 && index - 2 < play.current_list.lock().unwrap().len() {
|
if index > 1 && index - 2 < play_control.current_list.lock().unwrap().len() {
|
||||||
let media = play.current_list.lock().unwrap()[index - 2].clone();
|
let media = play_control.current_list.lock().unwrap()[index - 2].clone();
|
||||||
|
|
||||||
let data_map = get_data_map(config, media);
|
let data_map = get_data_map(config, media);
|
||||||
|
|
||||||
@ -201,10 +217,12 @@ pub fn json_rpc_server(
|
|||||||
Ok(Value::String("No, or wrong parameters set!".to_string()))
|
Ok(Value::String("No, or wrong parameters set!".to_string()))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// build rpc server
|
||||||
let server = ServerBuilder::new(io)
|
let server = ServerBuilder::new(io)
|
||||||
.cors(DomainsValidation::AllowOnly(vec![
|
.cors(DomainsValidation::AllowOnly(vec![
|
||||||
AccessControlAllowOrigin::Null,
|
AccessControlAllowOrigin::Null,
|
||||||
]))
|
]))
|
||||||
|
// add middleware, for authentication
|
||||||
.request_middleware(|request: hyper::Request<hyper::Body>| {
|
.request_middleware(|request: hyper::Request<hyper::Body>| {
|
||||||
if request.headers().contains_key("authorization")
|
if request.headers().contains_key("authorization")
|
||||||
&& request.headers()["authorization"] == config.rpc_server.authorization
|
&& request.headers()["authorization"] == config.rpc_server.authorization
|
||||||
@ -223,7 +241,7 @@ pub fn json_rpc_server(
|
|||||||
.start_http(&config.rpc_server.address.parse().unwrap())
|
.start_http(&config.rpc_server.address.parse().unwrap())
|
||||||
.expect("Unable to start RPC server");
|
.expect("Unable to start RPC server");
|
||||||
|
|
||||||
*proc_control.rpc_handle.lock().unwrap() = Some(server.close_handle().clone());
|
*proc_control.rpc_handle.lock().unwrap() = Some(server.close_handle());
|
||||||
|
|
||||||
server.wait();
|
server.wait();
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ pub struct Args {
|
|||||||
long,
|
long,
|
||||||
help = "Generate playlist for date. Date-range is possible, like: 2022-01-01 - 2022-01-10.",
|
help = "Generate playlist for date. Date-range is possible, like: 2022-01-01 - 2022-01-10.",
|
||||||
name = "YYYY-MM-DD",
|
name = "YYYY-MM-DD",
|
||||||
multiple_values=true
|
multiple_values = true
|
||||||
)]
|
)]
|
||||||
pub generate: Option<Vec<String>>,
|
pub generate: Option<Vec<String>>,
|
||||||
|
|
||||||
@ -54,8 +54,7 @@ pub struct Args {
|
|||||||
pub volume: Option<f64>,
|
pub volume: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get arguments from command line, and return them.
|
||||||
pub fn get_args() -> Args {
|
pub fn get_args() -> Args {
|
||||||
let args = Args::parse();
|
Args::parse()
|
||||||
|
|
||||||
args
|
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,9 @@ use shlex::split;
|
|||||||
|
|
||||||
use crate::utils::{get_args, time_to_sec};
|
use crate::utils::{get_args, time_to_sec};
|
||||||
|
|
||||||
|
/// Global Config
|
||||||
|
///
|
||||||
|
/// This we init ones, when ffplayout is starting and use them globally in the hole program.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct GlobalConfig {
|
pub struct GlobalConfig {
|
||||||
pub general: General,
|
pub general: General,
|
||||||
@ -78,6 +81,7 @@ pub struct Processing {
|
|||||||
pub logo_opacity: f32,
|
pub logo_opacity: f32,
|
||||||
pub logo_filter: String,
|
pub logo_filter: String,
|
||||||
pub add_loudnorm: bool,
|
pub add_loudnorm: bool,
|
||||||
|
pub loudnorm_ingest: bool,
|
||||||
pub loud_i: f32,
|
pub loud_i: f32,
|
||||||
pub loud_tp: f32,
|
pub loud_tp: f32,
|
||||||
pub loud_lra: f32,
|
pub loud_lra: f32,
|
||||||
@ -132,6 +136,7 @@ pub struct Out {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GlobalConfig {
|
impl GlobalConfig {
|
||||||
|
/// Read config from YAML file, and set some extra config values.
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let args = get_args();
|
let args = get_args();
|
||||||
let mut config_path = match env::current_exe() {
|
let mut config_path = match env::current_exe() {
|
||||||
@ -149,8 +154,7 @@ impl GlobalConfig {
|
|||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!(
|
println!(
|
||||||
"{config_path:?} doesn't exists!\n{}\n\nSystem error: {err}",
|
"{config_path:?} doesn't exists!\nPut \"ffplayout.yml\" in \"/etc/playout/\" or beside the executable!\n\nSystem error: {err}"
|
||||||
"Put \"ffplayout.yml\" in \"/etc/playout/\" or beside the executable!"
|
|
||||||
);
|
);
|
||||||
process::exit(0x0100);
|
process::exit(0x0100);
|
||||||
}
|
}
|
||||||
@ -167,12 +171,13 @@ impl GlobalConfig {
|
|||||||
let bitrate = config.processing.width * config.processing.height / 10;
|
let bitrate = config.processing.width * config.processing.height / 10;
|
||||||
config.playlist.start_sec = Some(time_to_sec(&config.playlist.day_start));
|
config.playlist.start_sec = Some(time_to_sec(&config.playlist.day_start));
|
||||||
|
|
||||||
if config.playlist.length.contains(":") {
|
if config.playlist.length.contains(':') {
|
||||||
config.playlist.length_sec = Some(time_to_sec(&config.playlist.length));
|
config.playlist.length_sec = Some(time_to_sec(&config.playlist.length));
|
||||||
} else {
|
} else {
|
||||||
config.playlist.length_sec = Some(86400.0);
|
config.playlist.length_sec = Some(86400.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We set the decoder settings here, so we only define them ones.
|
||||||
let mut settings: Vec<String> = vec![
|
let mut settings: Vec<String> = vec![
|
||||||
"-pix_fmt",
|
"-pix_fmt",
|
||||||
"yuv420p",
|
"yuv420p",
|
||||||
@ -209,6 +214,8 @@ impl GlobalConfig {
|
|||||||
config.out.preview_cmd = split(config.out.preview_param.as_str());
|
config.out.preview_cmd = split(config.out.preview_param.as_str());
|
||||||
config.out.output_cmd = split(config.out.output_param.as_str());
|
config.out.output_cmd = split(config.out.output_param.as_str());
|
||||||
|
|
||||||
|
// Read command line arguments, and override the config with them.
|
||||||
|
|
||||||
if let Some(gen) = args.generate {
|
if let Some(gen) = args.generate {
|
||||||
config.general.generate = Some(gen);
|
config.general.generate = Some(gen);
|
||||||
}
|
}
|
||||||
@ -237,7 +244,7 @@ impl GlobalConfig {
|
|||||||
if let Some(length) = args.length {
|
if let Some(length) = args.length {
|
||||||
config.playlist.length = length.clone();
|
config.playlist.length = length.clone();
|
||||||
|
|
||||||
if length.contains(":") {
|
if length.contains(':') {
|
||||||
config.playlist.length_sec = Some(time_to_sec(&length));
|
config.playlist.length_sec = Some(time_to_sec(&length));
|
||||||
} else {
|
} else {
|
||||||
config.playlist.length_sec = Some(86400.0);
|
config.playlist.length_sec = Some(86400.0);
|
||||||
@ -266,11 +273,10 @@ impl GlobalConfig {
|
|||||||
|
|
||||||
static INSTANCE: OnceCell<GlobalConfig> = OnceCell::new();
|
static INSTANCE: OnceCell<GlobalConfig> = OnceCell::new();
|
||||||
|
|
||||||
|
/// When add_loudnorm is False we use a different audio encoder,
|
||||||
|
/// s302m has higher quality, but is experimental
|
||||||
|
/// and works not well together with the loudnorm filter.
|
||||||
fn pre_audio_codec(add_loudnorm: bool) -> Vec<String> {
|
fn pre_audio_codec(add_loudnorm: bool) -> Vec<String> {
|
||||||
// when add_loudnorm is False we use a different audio encoder,
|
|
||||||
// s302m has higher quality, but is experimental
|
|
||||||
// and works not well together with the loudnorm filter
|
|
||||||
|
|
||||||
let mut codec = vec!["-c:a", "s302m", "-strict", "-2"];
|
let mut codec = vec!["-c:a", "s302m", "-strict", "-2"];
|
||||||
|
|
||||||
if add_loudnorm {
|
if add_loudnorm {
|
||||||
|
@ -12,6 +12,7 @@ use simplelog::*;
|
|||||||
|
|
||||||
use crate::utils::Media;
|
use crate::utils::Media;
|
||||||
|
|
||||||
|
/// Defined process units.
|
||||||
pub enum ProcessUnit {
|
pub enum ProcessUnit {
|
||||||
Decoder,
|
Decoder,
|
||||||
Encoder,
|
Encoder,
|
||||||
@ -30,6 +31,10 @@ impl fmt::Display for ProcessUnit {
|
|||||||
|
|
||||||
use ProcessUnit::*;
|
use ProcessUnit::*;
|
||||||
|
|
||||||
|
/// Process Controller
|
||||||
|
///
|
||||||
|
/// We save here some global states, about what is running and which processes are alive.
|
||||||
|
/// This we need for process termination, skipping clip decoder etc.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ProcessControl {
|
pub struct ProcessControl {
|
||||||
pub decoder_term: Arc<Mutex<Option<Child>>>,
|
pub decoder_term: Arc<Mutex<Option<Child>>>,
|
||||||
@ -88,6 +93,8 @@ impl ProcessControl {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wait for process to proper close.
|
||||||
|
/// This prevents orphaned/zombi processes in system
|
||||||
pub fn wait(&mut self, proc: ProcessUnit) -> Result<(), String> {
|
pub fn wait(&mut self, proc: ProcessUnit) -> Result<(), String> {
|
||||||
match proc {
|
match proc {
|
||||||
Decoder => {
|
Decoder => {
|
||||||
@ -116,6 +123,7 @@ impl ProcessControl {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// No matter what is running, terminate them all.
|
||||||
pub fn kill_all(&mut self) {
|
pub fn kill_all(&mut self) {
|
||||||
self.is_terminated.store(true, Ordering::SeqCst);
|
self.is_terminated.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
@ -141,6 +149,7 @@ impl Drop for ProcessControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Global player control, to get infos about current clip etc.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PlayerControl {
|
pub struct PlayerControl {
|
||||||
pub current_media: Arc<Mutex<Option<Media>>>,
|
pub current_media: Arc<Mutex<Option<Media>>>,
|
||||||
@ -158,6 +167,7 @@ impl PlayerControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Global playout control, for move forward/backward clip, or resetting playlist/state.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct PlayoutStatus {
|
pub struct PlayoutStatus {
|
||||||
pub time_shift: Arc<Mutex<f64>>,
|
pub time_shift: Arc<Mutex<f64>>,
|
||||||
|
@ -1,3 +1,11 @@
|
|||||||
|
/// Simple Playlist Generator
|
||||||
|
///
|
||||||
|
/// You can call ffplayout[.exe] -g YYYY-mm-dd - YYYY-mm-dd to generate JSON playlists.
|
||||||
|
///
|
||||||
|
/// The generator takes the files from storage, which are set in config.
|
||||||
|
/// It also respect the shuffle/sort mode.
|
||||||
|
///
|
||||||
|
/// Beside that it is really very basic, without any logic.
|
||||||
use std::{
|
use std::{
|
||||||
fs::{create_dir_all, write},
|
fs::{create_dir_all, write},
|
||||||
path::Path,
|
path::Path,
|
||||||
@ -8,33 +16,28 @@ use std::{
|
|||||||
use chrono::{Duration, NaiveDate};
|
use chrono::{Duration, NaiveDate};
|
||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
|
|
||||||
use crate::input::Source;
|
use crate::input::FolderSource;
|
||||||
use crate::utils::{json_serializer::Playlist, GlobalConfig, Media};
|
use crate::utils::{json_serializer::Playlist, GlobalConfig, Media};
|
||||||
|
|
||||||
fn get_date_range(date_range: &Vec<String>) -> Vec<String> {
|
/// Generate a vector with dates, from given range.
|
||||||
|
fn get_date_range(date_range: &[String]) -> Vec<String> {
|
||||||
let mut range = vec![];
|
let mut range = vec![];
|
||||||
let start;
|
|
||||||
let end;
|
|
||||||
|
|
||||||
match NaiveDate::parse_from_str(&date_range[0], "%Y-%m-%d") {
|
let start = match NaiveDate::parse_from_str(&date_range[0], "%Y-%m-%d") {
|
||||||
Ok(s) => {
|
Ok(s) => s,
|
||||||
start = s;
|
|
||||||
}
|
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
error!("date format error in: <yellow>{:?}</>", date_range[0]);
|
error!("date format error in: <yellow>{:?}</>", date_range[0]);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
match NaiveDate::parse_from_str(&date_range[2], "%Y-%m-%d") {
|
let end = match NaiveDate::parse_from_str(&date_range[2], "%Y-%m-%d") {
|
||||||
Ok(e) => {
|
Ok(e) => e,
|
||||||
end = e;
|
|
||||||
}
|
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
error!("date format error in: <yellow>{:?}</>", date_range[2]);
|
error!("date format error in: <yellow>{:?}</>", date_range[2]);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
let duration = end.signed_duration_since(start);
|
let duration = end.signed_duration_since(start);
|
||||||
let days = duration.num_days() + 1;
|
let days = duration.num_days() + 1;
|
||||||
@ -46,9 +49,10 @@ fn get_date_range(date_range: &Vec<String>) -> Vec<String> {
|
|||||||
range
|
range
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate playlists
|
||||||
pub fn generate_playlist(mut date_range: Vec<String>) {
|
pub fn generate_playlist(mut date_range: Vec<String>) {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
let total_length = config.playlist.length_sec.unwrap().clone();
|
let total_length = config.playlist.length_sec.unwrap();
|
||||||
let current_list = Arc::new(Mutex::new(vec![Media::new(0, "".to_string(), false)]));
|
let current_list = Arc::new(Mutex::new(vec![Media::new(0, "".to_string(), false)]));
|
||||||
let index = Arc::new(AtomicUsize::new(0));
|
let index = Arc::new(AtomicUsize::new(0));
|
||||||
let playlist_root = Path::new(&config.playlist.path);
|
let playlist_root = Path::new(&config.playlist.path);
|
||||||
@ -66,7 +70,7 @@ pub fn generate_playlist(mut date_range: Vec<String>) {
|
|||||||
date_range = get_date_range(&date_range)
|
date_range = get_date_range(&date_range)
|
||||||
}
|
}
|
||||||
|
|
||||||
let media_list = Source::new(current_list, index);
|
let media_list = FolderSource::new(current_list, index);
|
||||||
let list_length = media_list.nodes.lock().unwrap().len();
|
let list_length = media_list.nodes.lock().unwrap().len();
|
||||||
|
|
||||||
for date in date_range {
|
for date in date_range {
|
||||||
@ -96,7 +100,7 @@ pub fn generate_playlist(mut date_range: Vec<String>) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut filler = Media::new(0, config.storage.filler_clip.clone(), true);
|
let mut filler = Media::new(0, config.storage.filler_clip.clone(), true);
|
||||||
let filler_length = filler.duration.clone();
|
let filler_length = filler.duration;
|
||||||
let mut length = 0.0;
|
let mut length = 0.0;
|
||||||
let mut round = 0;
|
let mut round = 0;
|
||||||
|
|
||||||
@ -109,7 +113,7 @@ pub fn generate_playlist(mut date_range: Vec<String>) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for item in media_list.clone() {
|
for item in media_list.clone() {
|
||||||
let duration = item.duration.clone();
|
let duration = item.duration;
|
||||||
|
|
||||||
if total_length > length + duration {
|
if total_length > length + duration {
|
||||||
playlist.program.push(item);
|
playlist.program.push(item);
|
||||||
|
@ -12,6 +12,7 @@ use crate::utils::{get_date, modified_time, validate_playlist, GlobalConfig, Med
|
|||||||
|
|
||||||
pub const DUMMY_LEN: f64 = 60.0;
|
pub const DUMMY_LEN: f64 = 60.0;
|
||||||
|
|
||||||
|
/// This is our main playlist object, it holds all necessary information for the current day.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Playlist {
|
pub struct Playlist {
|
||||||
pub date: String,
|
pub date: String,
|
||||||
@ -44,6 +45,8 @@ impl Playlist {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read json playlist file, fills Playlist struct and set some extra values,
|
||||||
|
/// which we need to process.
|
||||||
pub fn read_json(
|
pub fn read_json(
|
||||||
path: Option<String>,
|
path: Option<String>,
|
||||||
is_terminated: Arc<AtomicBool>,
|
is_terminated: Arc<AtomicBool>,
|
||||||
@ -89,13 +92,14 @@ pub fn read_json(
|
|||||||
serde_json::from_reader(f).expect("Could not read json playlist file.");
|
serde_json::from_reader(f).expect("Could not read json playlist file.");
|
||||||
|
|
||||||
playlist.current_file = Some(current_file.clone());
|
playlist.current_file = Some(current_file.clone());
|
||||||
playlist.start_sec = Some(start_sec.clone());
|
playlist.start_sec = Some(start_sec);
|
||||||
let modify = modified_time(¤t_file);
|
let modify = modified_time(¤t_file);
|
||||||
|
|
||||||
if let Some(modi) = modify {
|
if let Some(modi) = modify {
|
||||||
playlist.modified = Some(modi.to_string());
|
playlist.modified = Some(modi.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add extra values to every media clip
|
||||||
for (i, item) in playlist.program.iter_mut().enumerate() {
|
for (i, item) in playlist.program.iter_mut().enumerate() {
|
||||||
item.begin = Some(start_sec);
|
item.begin = Some(start_sec);
|
||||||
item.index = Some(i);
|
item.index = Some(i);
|
||||||
|
@ -1,9 +1,22 @@
|
|||||||
use std::{path::Path, sync::{atomic::{AtomicBool, Ordering}, Arc}};
|
use std::{
|
||||||
|
path::Path,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
|
|
||||||
use crate::utils::{sec_to_time, GlobalConfig, MediaProbe, Playlist};
|
use crate::utils::{sec_to_time, GlobalConfig, MediaProbe, Playlist};
|
||||||
|
|
||||||
|
/// Validate a given playlist, to check if:
|
||||||
|
///
|
||||||
|
/// - the source files are existing
|
||||||
|
/// - file can be read by ffprobe and metadata exists
|
||||||
|
/// - total playtime fits target length from config
|
||||||
|
///
|
||||||
|
/// This function we run in a thread, to don't block the main function.
|
||||||
pub fn validate_playlist(playlist: Playlist, is_terminated: Arc<AtomicBool>, config: GlobalConfig) {
|
pub fn validate_playlist(playlist: Playlist, is_terminated: Arc<AtomicBool>, config: GlobalConfig) {
|
||||||
let date = playlist.date;
|
let date = playlist.date;
|
||||||
let mut length = config.playlist.length_sec.unwrap();
|
let mut length = config.playlist.length_sec.unwrap();
|
||||||
@ -15,7 +28,7 @@ pub fn validate_playlist(playlist: Playlist, is_terminated: Arc<AtomicBool>, con
|
|||||||
|
|
||||||
for item in playlist.program.iter() {
|
for item in playlist.program.iter() {
|
||||||
if is_terminated.load(Ordering::SeqCst) {
|
if is_terminated.load(Ordering::SeqCst) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if Path::new(&item.source).is_file() {
|
if Path::new(&item.source).is_file() {
|
||||||
|
@ -8,19 +8,23 @@ use std::{
|
|||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use file_rotate::{compression::Compression, suffix::AppendCount, ContentLimit, FileRotate};
|
use chrono::prelude::*;
|
||||||
|
use file_rotate::{
|
||||||
|
compression::Compression,
|
||||||
|
suffix::{AppendTimestamp, DateFrom, FileLimit},
|
||||||
|
ContentLimit, FileRotate, TimeFrequency,
|
||||||
|
};
|
||||||
use lettre::{
|
use lettre::{
|
||||||
message::header, transport::smtp::authentication::Credentials, Message, SmtpTransport,
|
message::header, transport::smtp::authentication::Credentials, Message, SmtpTransport,
|
||||||
Transport,
|
Transport,
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono::prelude::*;
|
|
||||||
use log::{Level, LevelFilter, Log, Metadata, Record};
|
use log::{Level, LevelFilter, Log, Metadata, Record};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
|
|
||||||
use crate::utils::GlobalConfig;
|
use crate::utils::GlobalConfig;
|
||||||
|
|
||||||
|
/// send log messages to mail recipient
|
||||||
fn send_mail(msg: String) {
|
fn send_mail(msg: String) {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
|
|
||||||
@ -52,9 +56,10 @@ fn send_mail(msg: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Basic Mail Queue
|
||||||
|
///
|
||||||
|
/// Check every give seconds for messages and send them.
|
||||||
fn mail_queue(messages: Arc<Mutex<Vec<String>>>, interval: u64) {
|
fn mail_queue(messages: Arc<Mutex<Vec<String>>>, interval: u64) {
|
||||||
// check every give seconds for messages and send them
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if messages.lock().unwrap().len() > 0 {
|
if messages.lock().unwrap().len() > 0 {
|
||||||
let msg = messages.lock().unwrap().join("\n");
|
let msg = messages.lock().unwrap().join("\n");
|
||||||
@ -67,6 +72,7 @@ fn mail_queue(messages: Arc<Mutex<Vec<String>>>, interval: u64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Self made Mail Log struct, to extend simplelog.
|
||||||
pub struct LogMailer {
|
pub struct LogMailer {
|
||||||
level: LevelFilter,
|
level: LevelFilter,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
@ -121,12 +127,20 @@ impl SharedLogger for LogMailer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Workaround to remove color information from log
|
||||||
|
///
|
||||||
|
/// ToDo: maybe in next version from simplelog this is not necessary anymore.
|
||||||
fn clean_string(text: &str) -> String {
|
fn clean_string(text: &str) -> String {
|
||||||
let regex: Regex = Regex::new(r"\x1b\[[0-9;]*[mGKF]").unwrap();
|
let regex: Regex = Regex::new(r"\x1b\[[0-9;]*[mGKF]").unwrap();
|
||||||
|
|
||||||
regex.replace_all(text, "").to_string()
|
regex.replace_all(text, "").to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initialize our logging, to have:
|
||||||
|
///
|
||||||
|
/// - console logger
|
||||||
|
/// - file logger
|
||||||
|
/// - mail logger
|
||||||
pub fn init_logging() -> Vec<Box<dyn SharedLogger>> {
|
pub fn init_logging() -> Vec<Box<dyn SharedLogger>> {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
let app_config = config.logging.clone();
|
let app_config = config.logging.clone();
|
||||||
@ -137,18 +151,26 @@ pub fn init_logging() -> Vec<Box<dyn SharedLogger>> {
|
|||||||
time_level = LevelFilter::Error;
|
time_level = LevelFilter::Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
let log_config = simplelog::ConfigBuilder::new()
|
let mut log_config = ConfigBuilder::new()
|
||||||
.set_thread_level(LevelFilter::Off)
|
.set_thread_level(LevelFilter::Off)
|
||||||
.set_target_level(LevelFilter::Off)
|
.set_target_level(LevelFilter::Off)
|
||||||
.set_level_padding(LevelPadding::Left)
|
.set_level_padding(LevelPadding::Left)
|
||||||
.set_time_to_local(app_config.local_time)
|
|
||||||
.set_time_level(time_level)
|
.set_time_level(time_level)
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
|
if app_config.local_time {
|
||||||
|
log_config = match log_config.set_time_offset_to_local() {
|
||||||
|
Ok(local) => local.clone(),
|
||||||
|
Err(_) => log_config,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
if app_config.log_to_file {
|
if app_config.log_to_file {
|
||||||
let file_config = log_config
|
let file_config = log_config
|
||||||
.clone()
|
.clone()
|
||||||
.set_time_format("[%Y-%m-%d %H:%M:%S%.3f]".into())
|
.set_time_format_custom(format_description!(
|
||||||
|
"[[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]]"
|
||||||
|
))
|
||||||
.build();
|
.build();
|
||||||
let mut log_path = "logs/ffplayout.log".to_string();
|
let mut log_path = "logs/ffplayout.log".to_string();
|
||||||
|
|
||||||
@ -166,8 +188,12 @@ pub fn init_logging() -> Vec<Box<dyn SharedLogger>> {
|
|||||||
let log = || {
|
let log = || {
|
||||||
FileRotate::new(
|
FileRotate::new(
|
||||||
log_path,
|
log_path,
|
||||||
AppendCount::new(app_config.backup_count),
|
AppendTimestamp::with_format(
|
||||||
ContentLimit::Lines(1000),
|
"%Y-%m-%d",
|
||||||
|
FileLimit::MaxFiles(app_config.backup_count),
|
||||||
|
DateFrom::DateYesterday,
|
||||||
|
),
|
||||||
|
ContentLimit::Time(TimeFrequency::Daily),
|
||||||
Compression::None,
|
Compression::None,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@ -180,7 +206,9 @@ pub fn init_logging() -> Vec<Box<dyn SharedLogger>> {
|
|||||||
.set_level_color(Level::Info, Some(Color::Ansi256(10)))
|
.set_level_color(Level::Info, Some(Color::Ansi256(10)))
|
||||||
.set_level_color(Level::Warn, Some(Color::Ansi256(208)))
|
.set_level_color(Level::Warn, Some(Color::Ansi256(208)))
|
||||||
.set_level_color(Level::Error, Some(Color::Ansi256(9)))
|
.set_level_color(Level::Error, Some(Color::Ansi256(9)))
|
||||||
.set_time_format_str("\x1b[30;1m[%Y-%m-%d %H:%M:%S%.3f]\x1b[0m")
|
.set_time_format_custom(format_description!(
|
||||||
|
"\x1b[[30;1m[[[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:4]]\x1b[[0m"
|
||||||
|
))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
app_logger.push(TermLogger::new(
|
app_logger.push(TermLogger::new(
|
||||||
@ -191,14 +219,15 @@ pub fn init_logging() -> Vec<Box<dyn SharedLogger>> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.mail.recipient.contains("@") && config.mail.recipient.contains(".") {
|
// set mail logger only the recipient is set in config
|
||||||
|
if config.mail.recipient.contains('@') && config.mail.recipient.contains('.') {
|
||||||
let messages: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
let messages: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||||
let messages_clone = messages.clone();
|
let messages_clone = messages.clone();
|
||||||
let interval = config.mail.interval.clone();
|
let interval = config.mail.interval;
|
||||||
|
|
||||||
thread::spawn(move || mail_queue(messages_clone, interval));
|
thread::spawn(move || mail_queue(messages_clone, interval));
|
||||||
|
|
||||||
let mail_config = log_config.clone().build();
|
let mail_config = log_config.build();
|
||||||
|
|
||||||
let filter = match config.mail.mail_level.to_lowercase().as_str() {
|
let filter = match config.mail.mail_level.to_lowercase().as_str() {
|
||||||
"info" => LevelFilter::Info,
|
"info" => LevelFilter::Info,
|
||||||
|
106
src/utils/mod.rs
106
src/utils/mod.rs
@ -35,6 +35,7 @@ pub use logging::init_logging;
|
|||||||
|
|
||||||
use crate::filter::filter_chains;
|
use crate::filter::filter_chains;
|
||||||
|
|
||||||
|
/// Video clip struct to hold some important states and comments for current media.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Media {
|
pub struct Media {
|
||||||
#[serde(skip_serializing, skip_deserializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
@ -89,12 +90,12 @@ impl Media {
|
|||||||
index: Some(index),
|
index: Some(index),
|
||||||
seek: 0.0,
|
seek: 0.0,
|
||||||
out: duration,
|
out: duration,
|
||||||
duration: duration,
|
duration,
|
||||||
category: None,
|
category: None,
|
||||||
source: src.clone(),
|
source: src.clone(),
|
||||||
cmd: Some(vec!["-i".to_string(), src]),
|
cmd: Some(vec!["-i".to_string(), src]),
|
||||||
filter: Some(vec![]),
|
filter: Some(vec![]),
|
||||||
probe: probe,
|
probe,
|
||||||
last_ad: Some(false),
|
last_ad: Some(false),
|
||||||
next_ad: Some(false),
|
next_ad: Some(false),
|
||||||
process: Some(true),
|
process: Some(true),
|
||||||
@ -124,6 +125,7 @@ impl Media {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// We use the ffprobe crate, but we map the metadata to our needs.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct MediaProbe {
|
pub struct MediaProbe {
|
||||||
pub format: Option<Format>,
|
pub format: Option<Format>,
|
||||||
@ -158,12 +160,12 @@ impl MediaProbe {
|
|||||||
|
|
||||||
MediaProbe {
|
MediaProbe {
|
||||||
format: Some(obj.format),
|
format: Some(obj.format),
|
||||||
audio_streams: if a_stream.len() > 0 {
|
audio_streams: if !a_stream.is_empty() {
|
||||||
Some(a_stream)
|
Some(a_stream)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
video_streams: if v_stream.len() > 0 {
|
video_streams: if !v_stream.is_empty() {
|
||||||
Some(v_stream)
|
Some(v_stream)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -185,6 +187,9 @@ impl MediaProbe {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write current status to status file in temp folder.
|
||||||
|
///
|
||||||
|
/// The status file is init in main function and mostly modified in RPC server.
|
||||||
pub fn write_status(date: &str, shift: f64) {
|
pub fn write_status(date: &str, shift: f64) {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
let stat_file = config.general.stat_file.clone();
|
let stat_file = config.general.stat_file.clone();
|
||||||
@ -206,6 +211,7 @@ pub fn write_status(date: &str, shift: f64) {
|
|||||||
// local.timestamp_millis() as i64
|
// local.timestamp_millis() as i64
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
/// Get current time in seconds.
|
||||||
pub fn get_sec() -> f64 {
|
pub fn get_sec() -> f64 {
|
||||||
let local: DateTime<Local> = Local::now();
|
let local: DateTime<Local> = Local::now();
|
||||||
|
|
||||||
@ -213,6 +219,10 @@ pub fn get_sec() -> f64 {
|
|||||||
+ (local.nanosecond() as f64 / 1000000000.0)
|
+ (local.nanosecond() as f64 / 1000000000.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get current date for playlist, but check time with conditions:
|
||||||
|
///
|
||||||
|
/// - When time is before playlist start, get date from yesterday.
|
||||||
|
/// - When given next_start is over target length (normally a full day), get date from tomorrow.
|
||||||
pub fn get_date(seek: bool, start: f64, next_start: f64) -> String {
|
pub fn get_date(seek: bool, start: f64, next_start: f64) -> String {
|
||||||
let local: DateTime<Local> = Local::now();
|
let local: DateTime<Local> = Local::now();
|
||||||
|
|
||||||
@ -227,6 +237,7 @@ pub fn get_date(seek: bool, start: f64, next_start: f64) -> String {
|
|||||||
local.format("%Y-%m-%d").to_string()
|
local.format("%Y-%m-%d").to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get file modification time.
|
||||||
pub fn modified_time(path: &str) -> Option<DateTime<Local>> {
|
pub fn modified_time(path: &str) -> Option<DateTime<Local>> {
|
||||||
let metadata = metadata(path).unwrap();
|
let metadata = metadata(path).unwrap();
|
||||||
|
|
||||||
@ -238,8 +249,9 @@ pub fn modified_time(path: &str) -> Option<DateTime<Local>> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a formatted time string to seconds.
|
||||||
pub fn time_to_sec(time_str: &str) -> f64 {
|
pub fn time_to_sec(time_str: &str) -> f64 {
|
||||||
if ["now", "", "none"].contains(&time_str) || !time_str.contains(":") {
|
if ["now", "", "none"].contains(&time_str) || !time_str.contains(':') {
|
||||||
return get_sec();
|
return get_sec();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,6 +263,7 @@ pub fn time_to_sec(time_str: &str) -> f64 {
|
|||||||
h * 3600.0 + m * 60.0 + s
|
h * 3600.0 + m * 60.0 + s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert floating number (seconds) to a formatted time string.
|
||||||
pub fn sec_to_time(sec: f64) -> String {
|
pub fn sec_to_time(sec: f64) -> String {
|
||||||
let d = UNIX_EPOCH + time::Duration::from_millis((sec * 1000.0) as u64);
|
let d = UNIX_EPOCH + time::Duration::from_millis((sec * 1000.0) as u64);
|
||||||
// Create DateTime from SystemTime
|
// Create DateTime from SystemTime
|
||||||
@ -259,6 +272,8 @@ pub fn sec_to_time(sec: f64) -> String {
|
|||||||
date_time.format("%H:%M:%S%.3f").to_string()
|
date_time.format("%H:%M:%S%.3f").to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test if given numbers are close to each other,
|
||||||
|
/// with a third number for setting the maximum range.
|
||||||
pub fn is_close(a: f64, b: f64, to: f64) -> bool {
|
pub fn is_close(a: f64, b: f64, to: f64) -> bool {
|
||||||
if (a - b).abs() < to {
|
if (a - b).abs() < to {
|
||||||
return true;
|
return true;
|
||||||
@ -267,13 +282,16 @@ pub fn is_close(a: f64, b: f64, to: f64) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get delta between clip start and current time. This value we need to check,
|
||||||
|
/// if we still in sync.
|
||||||
|
///
|
||||||
|
/// We also get here the global delta between clip start and time when a new playlist should start.
|
||||||
pub fn get_delta(begin: &f64) -> (f64, f64) {
|
pub fn get_delta(begin: &f64) -> (f64, f64) {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
let mut current_time = get_sec();
|
let mut current_time = get_sec();
|
||||||
let start = config.playlist.start_sec.unwrap();
|
let start = config.playlist.start_sec.unwrap();
|
||||||
let length = time_to_sec(&config.playlist.length);
|
let length = time_to_sec(&config.playlist.length);
|
||||||
let mut target_length = 86400.0;
|
let mut target_length = 86400.0;
|
||||||
let total_delta;
|
|
||||||
|
|
||||||
if length > 0.0 && length != target_length {
|
if length > 0.0 && length != target_length {
|
||||||
target_length = length
|
target_length = length
|
||||||
@ -290,15 +308,16 @@ pub fn get_delta(begin: &f64) -> (f64, f64) {
|
|||||||
current_delta -= 86400.0
|
current_delta -= 86400.0
|
||||||
}
|
}
|
||||||
|
|
||||||
if current_time < start {
|
let total_delta = if current_time < start {
|
||||||
total_delta = start - current_time;
|
start - current_time
|
||||||
} else {
|
} else {
|
||||||
total_delta = target_length + start - current_time;
|
target_length + start - current_time
|
||||||
}
|
};
|
||||||
|
|
||||||
(current_delta, total_delta)
|
(current_delta, total_delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if clip in playlist is in sync with global time.
|
||||||
pub fn check_sync(delta: f64) -> bool {
|
pub fn check_sync(delta: f64) -> bool {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
|
|
||||||
@ -310,6 +329,7 @@ pub fn check_sync(delta: f64) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a dummy clip as a placeholder for missing video files.
|
||||||
pub fn gen_dummy(duration: f64) -> (String, Vec<String>) {
|
pub fn gen_dummy(duration: f64) -> (String, Vec<String>) {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
let color = "#121212";
|
let color = "#121212";
|
||||||
@ -334,6 +354,7 @@ pub fn gen_dummy(duration: f64) -> (String, Vec<String>) {
|
|||||||
(source, cmd)
|
(source, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set clip seek in and length value.
|
||||||
pub fn seek_and_length(src: String, seek: f64, out: f64, duration: f64) -> Vec<String> {
|
pub fn seek_and_length(src: String, seek: f64, out: f64, duration: f64) -> Vec<String> {
|
||||||
let mut source_cmd: Vec<String> = vec![];
|
let mut source_cmd: Vec<String> = vec![];
|
||||||
|
|
||||||
@ -344,25 +365,19 @@ pub fn seek_and_length(src: String, seek: f64, out: f64, duration: f64) -> Vec<S
|
|||||||
source_cmd.append(&mut vec!["-i".to_string(), src]);
|
source_cmd.append(&mut vec!["-i".to_string(), src]);
|
||||||
|
|
||||||
if duration > out {
|
if duration > out {
|
||||||
source_cmd.append(&mut vec![
|
source_cmd.append(&mut vec!["-t".to_string(), format!("{}", out - seek)]);
|
||||||
"-t".to_string(),
|
|
||||||
format!("{}", out - seek).to_string(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
source_cmd
|
source_cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read ffmpeg stderr decoder, encoder and server instance
|
||||||
|
/// and log the output.
|
||||||
pub fn stderr_reader(buffer: BufReader<ChildStderr>, suffix: &str) -> Result<(), Error> {
|
pub fn stderr_reader(buffer: BufReader<ChildStderr>, suffix: &str) -> Result<(), Error> {
|
||||||
// read ffmpeg stderr decoder, encoder and server instance
|
|
||||||
// and log the output
|
|
||||||
|
|
||||||
fn format_line(line: String, level: &str) -> String {
|
fn format_line(line: String, level: &str) -> String {
|
||||||
line.replace(&format!("[{level: >5}] "), "")
|
line.replace(&format!("[{level: >5}] "), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// let buffer = BufReader::new(std_errors);
|
|
||||||
|
|
||||||
for line in buffer.lines() {
|
for line in buffer.lines() {
|
||||||
let line = line?;
|
let line = line?;
|
||||||
|
|
||||||
@ -373,22 +388,21 @@ pub fn stderr_reader(buffer: BufReader<ChildStderr>, suffix: &str) -> Result<(),
|
|||||||
"<bright black>[{suffix}]</> {}",
|
"<bright black>[{suffix}]</> {}",
|
||||||
format_line(line, "warning")
|
format_line(line, "warning")
|
||||||
)
|
)
|
||||||
} else {
|
} else if suffix != "server"
|
||||||
if suffix != "server"
|
&& !line.contains("Input/output error")
|
||||||
&& !line.contains("Input/output error")
|
&& !line.contains("Broken pipe")
|
||||||
&& !line.contains("Broken pipe")
|
{
|
||||||
{
|
error!(
|
||||||
error!(
|
"<bright black>[{suffix}]</> {}",
|
||||||
"<bright black>[{suffix}]</> {}",
|
format_line(line.clone(), "error")
|
||||||
format_line(line.clone(), "error")
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run program to test if it is in system.
|
||||||
fn is_in_system(name: &str) {
|
fn is_in_system(name: &str) {
|
||||||
if let Ok(mut proc) = Command::new(name)
|
if let Ok(mut proc) = Command::new(name)
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
@ -407,6 +421,8 @@ fn is_in_system(name: &str) {
|
|||||||
fn ffmpeg_libs_and_filter() -> (Vec<String>, Vec<String>) {
|
fn ffmpeg_libs_and_filter() -> (Vec<String>, Vec<String>) {
|
||||||
let mut libs: Vec<String> = vec![];
|
let mut libs: Vec<String> = vec![];
|
||||||
let mut filters: Vec<String> = vec![];
|
let mut filters: Vec<String> = vec![];
|
||||||
|
|
||||||
|
// filter lines which contains filter
|
||||||
let re: Regex = Regex::new(r"^( ?) [TSC.]+").unwrap();
|
let re: Regex = Regex::new(r"^( ?) [TSC.]+").unwrap();
|
||||||
|
|
||||||
let mut ff_proc = match Command::new("ffmpeg")
|
let mut ff_proc = match Command::new("ffmpeg")
|
||||||
@ -425,27 +441,27 @@ fn ffmpeg_libs_and_filter() -> (Vec<String>, Vec<String>) {
|
|||||||
let err_buffer = BufReader::new(ff_proc.stderr.take().unwrap());
|
let err_buffer = BufReader::new(ff_proc.stderr.take().unwrap());
|
||||||
let out_buffer = BufReader::new(ff_proc.stdout.take().unwrap());
|
let out_buffer = BufReader::new(ff_proc.stdout.take().unwrap());
|
||||||
|
|
||||||
for line in err_buffer.lines() {
|
// stderr shows only the ffmpeg configuration
|
||||||
if let Ok(line) = line {
|
// get codec library's
|
||||||
if line.contains("configuration:") {
|
for line in err_buffer.lines().flatten() {
|
||||||
let configs = line.split_whitespace();
|
if line.contains("configuration:") {
|
||||||
|
let configs = line.split_whitespace();
|
||||||
|
|
||||||
for config in configs {
|
for config in configs {
|
||||||
if config.contains("--enable-lib") {
|
if config.contains("--enable-lib") {
|
||||||
libs.push(config.replace("--enable-", ""));
|
libs.push(config.replace("--enable-", ""));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for line in out_buffer.lines() {
|
// stdout shows filter help text
|
||||||
if let Ok(line) = line {
|
// get filters
|
||||||
if let Some(_) = re.captures(line.as_str()) {
|
for line in out_buffer.lines().flatten() {
|
||||||
let filter_line = line.split_whitespace();
|
if re.captures(line.as_str()).is_some() {
|
||||||
|
let filter_line = line.split_whitespace();
|
||||||
|
|
||||||
filters.push(filter_line.collect::<Vec<&str>>()[1].to_string());
|
filters.push(filter_line.collect::<Vec<&str>>()[1].to_string());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,6 +471,10 @@ fn ffmpeg_libs_and_filter() -> (Vec<String>, Vec<String>) {
|
|||||||
|
|
||||||
(libs, filters)
|
(libs, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate ffmpeg/ffprobe/ffplay.
|
||||||
|
///
|
||||||
|
/// Check if they are in system and has all filters and codecs we need.
|
||||||
pub fn validate_ffmpeg() {
|
pub fn validate_ffmpeg() {
|
||||||
let config = GlobalConfig::global();
|
let config = GlobalConfig::global();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user