Merge pull request #186 from jb-alvarado/master
support animated logo, add custom filter paramter in config and playlist
This commit is contained in:
commit
775ca7750b
113
Cargo.lock
generated
113
Cargo.lock
generated
@ -217,7 +217,7 @@ dependencies = [
|
||||
"serde_urlencoded",
|
||||
"smallvec",
|
||||
"socket2",
|
||||
"time 0.3.13",
|
||||
"time 0.3.14",
|
||||
"url",
|
||||
]
|
||||
|
||||
@ -301,9 +301,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@ -362,9 +362,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-global-executor"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5262ed948da60dd8956c6c5aca4d4163593dddb7b32d73267c93dab7b2e98940"
|
||||
checksum = "0da5b41ee986eed3f524c380e6d64965aea573882a8907682ad100f7859305ca"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-executor",
|
||||
@ -372,7 +372,6 @@ dependencies = [
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
@ -502,9 +501,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.5.1"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bdca834647821e0b13d9539a8634eb62d3501b6b6c2cec1722786ee6671b851"
|
||||
checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
@ -645,9 +644,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.17"
|
||||
version = "3.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b"
|
||||
checksum = "b15f2ea93df33549dbe2e8eecd1ca55269d63ae0b3ba1f55db030817d1c2867f"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
@ -662,9 +661,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "3.2.17"
|
||||
version = "3.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13547f7012c01ab4a0e8f8967730ada8f9fdf419e8b6c792788f39cf4e46eefa"
|
||||
checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
@ -704,7 +703,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time 0.3.13",
|
||||
"time 0.3.14",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
@ -726,9 +725,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1079fb8528d9f9c888b1e8aa651e6e079ade467323d58f75faf1d30b1808f540"
|
||||
checksum = "dc948ebb96241bb40ab73effeb80d9f93afaad49359d159a5e61be51619fe813"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@ -865,9 +864,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.1"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e851a83c30366fd01d75b913588e95e74a1705c1ecc5d58b1f8e1a6d556525f"
|
||||
checksum = "da3db6fcad7c1fc4abdd99bf5276a4db30d6a819127903a709ed41e5ff016e84"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
]
|
||||
@ -942,7 +941,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ffplayout"
|
||||
version = "0.14.3"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
@ -991,7 +990,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ffplayout-lib"
|
||||
version = "0.14.3"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"crossbeam-channel",
|
||||
@ -1010,7 +1009,7 @@ dependencies = [
|
||||
"serde_yaml",
|
||||
"shlex",
|
||||
"simplelog",
|
||||
"time 0.3.13",
|
||||
"time 0.3.14",
|
||||
"walkdir",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
@ -1143,9 +1142,9 @@ checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.23"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab30e97ab6aacfe635fad58f22c2bb06c8b685f7421eb1e064a729e2a5f481fa"
|
||||
checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@ -1158,9 +1157,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.23"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bfc52cbddcfd745bf1740338492bb0bd83d76c67b445f91c5fb29fae29ecaa1"
|
||||
checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@ -1168,15 +1167,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.23"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115"
|
||||
checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.23"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d11aa21b5b587a64682c0094c2bdd4df0076c5324961a40cc3abd7f37930528"
|
||||
checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@ -1196,9 +1195,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.23"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93a66fc6d035a26a3ae255a6d2bca35eda63ae4c5512bef54449113f7a1228e5"
|
||||
checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@ -1217,9 +1216,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.23"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0db9cce532b0eae2ccf2766ab246f114b56b9cf6d445e00c2549fbc100ca045d"
|
||||
checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1228,21 +1227,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.23"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca0bae1fe9752cf7fd9b0064c674ae63f97b37bc714d745cbde0afb7ec4e6765"
|
||||
checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.23"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306"
|
||||
checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.23"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577"
|
||||
checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@ -1453,13 +1452,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.46"
|
||||
version = "0.1.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501"
|
||||
checksum = "4c495f162af0bf17656d0014a0eded5f3cd2f365fdd204548c2869db89359dc7"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
@ -1727,9 +1727,9 @@ checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.7"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
|
||||
checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
@ -2154,10 +2154,11 @@ checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259"
|
||||
checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"log",
|
||||
@ -2446,18 +2447,18 @@ checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.143"
|
||||
version = "1.0.144"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553"
|
||||
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.143"
|
||||
version = "1.0.144"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391"
|
||||
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2466,9 +2467,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.83"
|
||||
version = "1.0.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7"
|
||||
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@ -2545,7 +2546,7 @@ dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"thiserror",
|
||||
"time 0.3.13",
|
||||
"time 0.3.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2557,7 +2558,7 @@ dependencies = [
|
||||
"log",
|
||||
"paris",
|
||||
"termcolor",
|
||||
"time 0.3.13",
|
||||
"time 0.3.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2577,9 +2578,9 @@ checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.4"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
|
||||
checksum = "10c98bba371b9b22a71a9414e420f92ddeb2369239af08200816169d5e2dd7aa"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi 0.3.9",
|
||||
@ -2790,9 +2791,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.13"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db76ff9fa4b1458b3c7f077f3ff9887394058460d21e634355b273aaf11eea45"
|
||||
checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"libc",
|
||||
|
@ -47,6 +47,7 @@ Check the [releases](https://github.com/ffplayout/ffplayout/releases/latest) for
|
||||
- [live ingest](/docs/live_ingest.md)
|
||||
- image source (will loop until out duration is reached)
|
||||
- extra audio source (experimental) (has priority over audio from video source)
|
||||
- [custom filter](/docs/custom_filters.md) globally in config, or in playlist for specific clips
|
||||
|
||||
For preview stream, read: [/docs/preview_stream.md](/docs/preview_stream.md)
|
||||
|
||||
@ -77,11 +78,17 @@ Check [install](docs/install.md) for details about how to install ffplayout.
|
||||
"out": 647.68,
|
||||
"duration": 647.68,
|
||||
"source": "/Media/clip1.mp4"
|
||||
}, {
|
||||
"in": 0,
|
||||
"out": 890.02,
|
||||
"duration": 890.02,
|
||||
"source": "/Media/clip2.mp4",
|
||||
"custom_filter": "eq=gamma_b=0.6:gamma_g=0.7[c_v_out]"
|
||||
}, {
|
||||
"in": 0,
|
||||
"out": 149,
|
||||
"duration": 149,
|
||||
"source": "/Media/clip2.mp4",
|
||||
"source": "/Media/clip3.mp4",
|
||||
"category": "advertisement"
|
||||
}, {
|
||||
"in": 0,
|
||||
|
@ -50,7 +50,9 @@ processing:
|
||||
scaling. With 'logo_opacity' logo can become transparent. With 'logo_filter'
|
||||
'overlay=W-w-12:12' you can modify the logo position. With 'use_loudnorm'
|
||||
you can activate single pass EBU R128 loudness normalization. 'loud_*' can
|
||||
adjust the loudnorm filter.
|
||||
adjust the loudnorm filter. With 'custom_filter' it is possible, to apply further
|
||||
filters. The filter outputs should end with [c_v_out] for video filter,
|
||||
and [c_a_out] for audio filter.
|
||||
mode: playlist
|
||||
width: 1024
|
||||
height: 576
|
||||
@ -67,6 +69,7 @@ processing:
|
||||
loud_tp: -1.5
|
||||
loud_lra: 11
|
||||
volume: 1
|
||||
custom_filter:
|
||||
|
||||
ingest:
|
||||
help_text: Run a server for a ingest stream. This stream will override the normal streaming
|
||||
|
@ -21,6 +21,10 @@ Using live ingest to inject a live stream.
|
||||
|
||||
The different output modes.
|
||||
|
||||
### **[Custom Filter](/docs/custom_filters.md)**
|
||||
|
||||
Apply self defined audio/video filters.
|
||||
|
||||
### **[Preview Stream](/docs/preview_stream.md)**
|
||||
|
||||
Setup and use a preview stream.
|
||||
|
30
docs/custom_filters.md
Normal file
30
docs/custom_filters.md
Normal file
@ -0,0 +1,30 @@
|
||||
## Custom filter
|
||||
|
||||
ffplayout allows it to define a custom filter string. For that is the parameter **custom_filter** in the **ffplayout.yml** config file. The playlist can also contain a **custom_filter** parameter for every clip, with the same usage.
|
||||
|
||||
The filter outputs should end with `[c_v_out]` for video filter, and `[c_a_out]` for audio filter. The filters will be apply on every clip and after the filters which unify the clips.
|
||||
|
||||
It is possible to apply only video or audio filters, or both. For a better understanding here some examples:
|
||||
|
||||
#### Apply Gaussian blur and volume filter:
|
||||
|
||||
```YAML
|
||||
custom_filter: 'gblur=5[c_v_out];volume=0.5[c_a_out]'
|
||||
```
|
||||
|
||||
#### Add lower third:
|
||||
|
||||
```YAML
|
||||
custom_filter: '[v_in];movie=/path/to/lower_third.png:loop=0,scale=1024:576,setpts=N/(25*TB)[lower];[v_in][lower]overlay=0:0:shortest=1[c_v_out]'
|
||||
```
|
||||
|
||||
Pay attention to the filter prefix `[v_in];`, this is necessary to get the output from the regular filters.
|
||||
|
||||
#### Paint effect
|
||||
|
||||
```YAML
|
||||
custom_filter: edgedetect=mode=colormix:high=0[c_v_out]
|
||||
```
|
||||
|
||||
Check ffmpeg [filters](https://ffmpeg.org/ffmpeg-filters.html) documentation, and find out which other filters ffmpeg has.
|
||||
|
@ -4,7 +4,7 @@ description = "24/7 playout based on rust and ffmpeg"
|
||||
license = "GPL-3.0"
|
||||
authors = ["Jonathan Baecker jonbae77@gmail.com"]
|
||||
readme = "README.md"
|
||||
version = "0.14.3"
|
||||
version = "0.15.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
@ -9,8 +9,10 @@ use std::{
|
||||
use crossbeam_channel::Sender;
|
||||
use simplelog::*;
|
||||
|
||||
use ffplayout_lib::filter::ingest_filter::filter_cmd;
|
||||
use ffplayout_lib::utils::{format_log_line, test_tcp_port, Ingest, PlayoutConfig, ProcessControl};
|
||||
use ffplayout_lib::filter::filter_chains;
|
||||
use ffplayout_lib::utils::{
|
||||
format_log_line, test_tcp_port, Ingest, Media, PlayoutConfig, ProcessControl,
|
||||
};
|
||||
use ffplayout_lib::vec_strings;
|
||||
|
||||
pub fn log_line(line: String, level: &str) {
|
||||
@ -82,9 +84,12 @@ pub fn ingest_server(
|
||||
let mut buffer: [u8; 65088] = [0; 65088];
|
||||
let mut server_cmd = vec_strings!["-hide_banner", "-nostats", "-v", "level+info"];
|
||||
let stream_input = config.ingest.input_cmd.clone().unwrap();
|
||||
let mut dummy_media = Media::new(0, "Live Stream".to_string(), false);
|
||||
dummy_media.is_live = Some(true);
|
||||
let mut filters = filter_chains(&config, &mut dummy_media, &Arc::new(Mutex::new(vec![])));
|
||||
|
||||
server_cmd.append(&mut stream_input.clone());
|
||||
server_cmd.append(&mut filter_cmd(&config, &Arc::new(Mutex::new(vec![]))));
|
||||
server_cmd.append(&mut filters);
|
||||
server_cmd.append(&mut config.processing.settings.unwrap());
|
||||
|
||||
let mut is_running;
|
||||
|
@ -26,8 +26,7 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child {
|
||||
|
||||
let mut filter: String = "null,".to_string();
|
||||
filter.push_str(
|
||||
v_drawtext::filter_node(config, None, &Arc::new(Mutex::new(vec![])), false)
|
||||
.as_str(),
|
||||
v_drawtext::filter_node(config, None, &Arc::new(Mutex::new(vec![]))).as_str(),
|
||||
);
|
||||
enc_filter = vec!["-vf".to_string(), filter];
|
||||
}
|
||||
|
@ -28,10 +28,10 @@ use std::{
|
||||
use simplelog::*;
|
||||
|
||||
use crate::input::{ingest::log_line, source_generator};
|
||||
use ffplayout_lib::filter::ingest_filter::filter_cmd;
|
||||
use ffplayout_lib::filter::filter_chains;
|
||||
use ffplayout_lib::utils::{
|
||||
prepare_output_cmd, sec_to_time, stderr_reader, test_tcp_port, Decoder, Ingest, PlayerControl,
|
||||
PlayoutConfig, PlayoutStatus, ProcessControl,
|
||||
prepare_output_cmd, sec_to_time, stderr_reader, test_tcp_port, Decoder, Ingest, Media,
|
||||
PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl,
|
||||
};
|
||||
use ffplayout_lib::vec_strings;
|
||||
|
||||
@ -47,28 +47,8 @@ fn ingest_to_hls_server(
|
||||
let mut server_prefix = vec_strings!["-hide_banner", "-nostats", "-v", "level+info"];
|
||||
let stream_input = config.ingest.input_cmd.clone().unwrap();
|
||||
server_prefix.append(&mut stream_input.clone());
|
||||
let server_filter = filter_cmd(&config, &playout_stat.chain);
|
||||
|
||||
if server_filter.len() > 1 {
|
||||
let filter_chain = server_filter[1]
|
||||
.split_terminator([',', ';'])
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
for (i, link) in filter_chain.iter().enumerate() {
|
||||
if link.contains("drawtext") {
|
||||
playout_stat
|
||||
.drawtext_server_index
|
||||
.store(i, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let server_cmd = prepare_output_cmd(
|
||||
server_prefix,
|
||||
server_filter,
|
||||
config.out.clone().output_cmd.unwrap(),
|
||||
"hls",
|
||||
);
|
||||
let mut dummy_media = Media::new(0, "Live Stream".to_string(), false);
|
||||
dummy_media.is_live = Some(true);
|
||||
|
||||
let mut is_running;
|
||||
|
||||
@ -81,13 +61,37 @@ fn ingest_to_hls_server(
|
||||
info!("Start ingest server, listening on: <b><magenta>{url}</></b>");
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Server CMD: <bright-blue>\"ffmpeg {}\"</>",
|
||||
server_cmd.join(" ")
|
||||
);
|
||||
|
||||
loop {
|
||||
let mut proc_ctl = proc_control.clone();
|
||||
let filters = filter_chains(&config, &mut dummy_media, &playout_stat.chain);
|
||||
|
||||
if filters.len() > 1 {
|
||||
// get correct filter index from drawtext node for zmq
|
||||
let filter_chain = filters[1]
|
||||
.split_terminator([',', ';'])
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
for (i, link) in filter_chain.iter().enumerate() {
|
||||
if link.contains("drawtext") {
|
||||
playout_stat
|
||||
.drawtext_server_index
|
||||
.store(i, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let server_cmd = prepare_output_cmd(
|
||||
server_prefix.clone(),
|
||||
filters,
|
||||
config.out.clone().output_cmd.unwrap(),
|
||||
"hls",
|
||||
);
|
||||
|
||||
debug!(
|
||||
"Server CMD: <bright-blue>\"ffmpeg {}\"</>",
|
||||
server_cmd.join(" ")
|
||||
);
|
||||
|
||||
let proc_ctl = proc_control.clone();
|
||||
let mut server_proc = match Command::new("ffmpeg")
|
||||
.args(server_cmd.clone())
|
||||
.stderr(Stdio::piped())
|
||||
@ -128,6 +132,10 @@ fn ingest_to_hls_server(
|
||||
log_line(line, &level);
|
||||
}
|
||||
|
||||
if proc_control.server_is_running.load(Ordering::SeqCst) {
|
||||
info!("Switch from live ingest to {}", config.processing.mode);
|
||||
}
|
||||
|
||||
proc_control
|
||||
.server_is_running
|
||||
.store(false, Ordering::SeqCst);
|
||||
@ -139,8 +147,6 @@ fn ingest_to_hls_server(
|
||||
if proc_control.is_terminated.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
|
||||
info!("Switch from live ingest to {}", config.processing.mode);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -221,7 +227,7 @@ pub fn write_hls(
|
||||
let dec_err = BufReader::new(enc_proc.stderr.take().unwrap());
|
||||
*proc_control.decoder_term.lock().unwrap() = Some(enc_proc);
|
||||
|
||||
if let Err(e) = stderr_reader(dec_err, "Writer") {
|
||||
if let Err(e) = stderr_reader(dec_err, "Writer", proc_control.clone()) {
|
||||
error!("{e:?}")
|
||||
};
|
||||
|
||||
|
@ -64,10 +64,11 @@ pub fn player(
|
||||
let mut enc_writer = BufWriter::new(enc_proc.stdin.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"));
|
||||
|
||||
*proc_control.encoder_term.lock().unwrap() = Some(enc_proc);
|
||||
let enc_p_ctl = proc_control.clone();
|
||||
|
||||
// spawn a thread to log ffmpeg output error messages
|
||||
let error_encoder_thread = thread::spawn(move || stderr_reader(enc_err, "Encoder", enc_p_ctl));
|
||||
|
||||
let proc_control_c = proc_control.clone();
|
||||
let mut ingest_receiver = None;
|
||||
@ -129,9 +130,12 @@ pub fn player(
|
||||
|
||||
let mut dec_reader = BufReader::new(dec_proc.stdout.take().unwrap());
|
||||
let dec_err = BufReader::new(dec_proc.stderr.take().unwrap());
|
||||
let error_decoder_thread = thread::spawn(move || stderr_reader(dec_err, "Decoder"));
|
||||
|
||||
*proc_control.decoder_term.lock().unwrap() = Some(dec_proc);
|
||||
let dec_p_ctl = proc_control.clone();
|
||||
|
||||
let error_decoder_thread =
|
||||
thread::spawn(move || stderr_reader(dec_err, "Decoder", dec_p_ctl));
|
||||
|
||||
loop {
|
||||
// when server is running, read from channel
|
||||
|
@ -35,8 +35,7 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child {
|
||||
|
||||
let mut filter: String = "null,".to_string();
|
||||
filter.push_str(
|
||||
v_drawtext::filter_node(config, None, &Arc::new(Mutex::new(vec![])), false)
|
||||
.as_str(),
|
||||
v_drawtext::filter_node(config, None, &Arc::new(Mutex::new(vec![]))).as_str(),
|
||||
);
|
||||
enc_filter = vec!["-vf".to_string(), filter];
|
||||
}
|
||||
|
@ -37,8 +37,7 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child {
|
||||
let mut filter = "[0:v]null,".to_string();
|
||||
|
||||
filter.push_str(
|
||||
v_drawtext::filter_node(config, None, &Arc::new(Mutex::new(vec![])), false)
|
||||
.as_str(),
|
||||
v_drawtext::filter_node(config, None, &Arc::new(Mutex::new(vec![]))).as_str(),
|
||||
);
|
||||
|
||||
enc_filter = vec!["-filter_complex".to_string(), filter];
|
||||
|
@ -12,8 +12,8 @@ use serde_json::{json, Map};
|
||||
use simplelog::*;
|
||||
|
||||
use ffplayout_lib::utils::{
|
||||
get_delta, get_filter_from_json, get_sec, sec_to_time, write_status, Media, PlayerControl,
|
||||
PlayoutConfig, PlayoutStatus, ProcessControl,
|
||||
get_delta, get_filter_from_json, get_sec, sec_to_time, write_status, Ingest, Media,
|
||||
PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl,
|
||||
};
|
||||
|
||||
use zmq_cmd::zmq_send;
|
||||
@ -90,6 +90,24 @@ pub fn json_rpc_server(
|
||||
let mut clips_filter = playout_stat.chain.lock().unwrap();
|
||||
*clips_filter = vec![filter.clone()];
|
||||
|
||||
if config.out.mode == "hls" {
|
||||
if proc.server_is_running.load(Ordering::SeqCst) {
|
||||
let filter_server = format!(
|
||||
"Parsed_drawtext_{} reinit {filter}",
|
||||
playout_stat.drawtext_server_index.load(Ordering::SeqCst)
|
||||
);
|
||||
|
||||
if let Ok(reply) = block_on(zmq_send(
|
||||
&filter_server,
|
||||
&config.text.zmq_server_socket.clone().unwrap(),
|
||||
)) {
|
||||
return Ok(Value::String(reply));
|
||||
};
|
||||
} else if let Err(e) = proc.kill(Ingest) {
|
||||
error!("Ingest {e:?}")
|
||||
}
|
||||
}
|
||||
|
||||
if config.out.mode != "hls" || !proc.server_is_running.load(Ordering::SeqCst) {
|
||||
let filter_stream = format!(
|
||||
"Parsed_drawtext_{} reinit {filter}",
|
||||
@ -103,20 +121,6 @@ pub fn json_rpc_server(
|
||||
return Ok(Value::String(reply));
|
||||
};
|
||||
}
|
||||
|
||||
if config.out.mode == "hls" && proc.server_is_running.load(Ordering::SeqCst) {
|
||||
let filter_server = format!(
|
||||
"Parsed_drawtext_{} reinit {filter}",
|
||||
playout_stat.drawtext_server_index.load(Ordering::SeqCst)
|
||||
);
|
||||
|
||||
if let Ok(reply) = block_on(zmq_send(
|
||||
&filter_server,
|
||||
&config.text.zmq_server_socket.clone().unwrap(),
|
||||
)) {
|
||||
return Ok(Value::String(reply));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(Value::String("Last clip can not be skipped".to_string()));
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit ec78765c0bfbe583ff9b5cbdcd6d011ecb89ac62
|
||||
Subproject commit cb20817330c5fd837eec1ae5716aa60f28f2b973
|
@ -4,7 +4,7 @@ description = "Library for ffplayout"
|
||||
license = "GPL-3.0"
|
||||
authors = ["Jonathan Baecker jonbae77@gmail.com"]
|
||||
readme = "README.md"
|
||||
version = "0.14.3"
|
||||
version = "0.15.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
46
lib/src/filter/custom_filter.rs
Normal file
46
lib/src/filter/custom_filter.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use simplelog::*;
|
||||
|
||||
fn strip_str(mut input: &str) -> String {
|
||||
input = input.strip_prefix(';').unwrap_or(input);
|
||||
input = input.strip_prefix("[0:v]").unwrap_or(input);
|
||||
input = input.strip_prefix("[0:a]").unwrap_or(input);
|
||||
input = input.strip_suffix(';').unwrap_or(input);
|
||||
input = input.strip_suffix("[c_v_out]").unwrap_or(input);
|
||||
input = input.strip_suffix("[c_a_out]").unwrap_or(input);
|
||||
|
||||
input.to_string()
|
||||
}
|
||||
|
||||
/// Apply custom filters
|
||||
pub fn custom_filter(filter: &str) -> (String, String) {
|
||||
let mut video_filter = String::new();
|
||||
let mut audio_filter = String::new();
|
||||
|
||||
if filter.contains("[c_v_out]") && filter.contains("[c_a_out]") {
|
||||
let v_pos = filter.find("[c_v_out]").unwrap();
|
||||
let a_pos = filter.find("[c_a_out]").unwrap();
|
||||
let mut delimiter = "[c_v_out]";
|
||||
|
||||
if v_pos > a_pos {
|
||||
delimiter = "[c_a_out]";
|
||||
}
|
||||
|
||||
if let Some((f_1, f_2)) = filter.split_once(delimiter) {
|
||||
if f_2.contains("[c_a_out]") {
|
||||
video_filter = strip_str(f_1);
|
||||
audio_filter = strip_str(f_2);
|
||||
} else {
|
||||
video_filter = strip_str(f_2);
|
||||
audio_filter = strip_str(f_1);
|
||||
}
|
||||
}
|
||||
} else if filter.contains("[c_v_out]") {
|
||||
video_filter = strip_str(filter);
|
||||
} else if filter.contains("[c_a_out]") {
|
||||
audio_filter = strip_str(filter);
|
||||
} else if !filter.is_empty() && filter != "~" {
|
||||
error!("Custom filter is not well formatted, use correct out link names (\"[c_v_out]\" and/or \"[c_a_out]\"). Filter skipped!")
|
||||
}
|
||||
|
||||
(video_filter, audio_filter)
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::filter::{a_loudnorm, v_drawtext, v_overlay};
|
||||
use crate::utils::PlayoutConfig;
|
||||
|
||||
/// Audio Filter
|
||||
///
|
||||
/// If needed we add audio filters to the server instance.
|
||||
fn audio_filter(config: &PlayoutConfig) -> String {
|
||||
let mut audio_chain = ";[0:a]afade=in:st=0:d=0.5".to_string();
|
||||
|
||||
if config.processing.loudnorm_ingest {
|
||||
audio_chain.push(',');
|
||||
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(config: &PlayoutConfig, filter_chain: &Arc<Mutex<Vec<String>>>) -> Vec<String> {
|
||||
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);
|
||||
let drawtext = v_drawtext::filter_node(config, None, filter_chain, true);
|
||||
|
||||
if !overlay.is_empty() {
|
||||
filter.push(',');
|
||||
filter.push_str(&overlay);
|
||||
}
|
||||
|
||||
if config.out.mode == "hls" && !drawtext.is_empty() {
|
||||
filter.push(',');
|
||||
filter.push_str(&drawtext);
|
||||
}
|
||||
|
||||
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(),
|
||||
]
|
||||
}
|
@ -6,11 +6,21 @@ use std::{
|
||||
use simplelog::*;
|
||||
|
||||
pub mod a_loudnorm;
|
||||
pub mod ingest_filter;
|
||||
pub mod custom_filter;
|
||||
pub mod v_drawtext;
|
||||
pub mod v_overlay;
|
||||
|
||||
use crate::utils::{get_delta, is_close, Media, MediaProbe, PlayoutConfig};
|
||||
use crate::utils::{fps_calc, get_delta, is_close, Media, MediaProbe, PlayoutConfig};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum FilterType {
|
||||
Audio,
|
||||
Video,
|
||||
}
|
||||
|
||||
use FilterType::*;
|
||||
|
||||
use self::custom_filter::custom_filter;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Filters {
|
||||
@ -25,14 +35,14 @@ impl Filters {
|
||||
Filters {
|
||||
audio_chain: None,
|
||||
video_chain: None,
|
||||
audio_map: "1:a".to_string(),
|
||||
audio_map: "0:a".to_string(),
|
||||
video_map: "0:v".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_filter(&mut self, filter: &str, codec_type: &str) {
|
||||
fn add_filter(&mut self, filter: &str, codec_type: FilterType) {
|
||||
match codec_type {
|
||||
"audio" => match &self.audio_chain {
|
||||
Audio => match &self.audio_chain {
|
||||
Some(ac) => {
|
||||
if filter.starts_with(';') || filter.starts_with('[') {
|
||||
self.audio_chain = Some(format!("{ac}{filter}"))
|
||||
@ -49,7 +59,7 @@ impl Filters {
|
||||
self.audio_map = "[aout1]".to_string();
|
||||
}
|
||||
},
|
||||
"video" => match &self.video_chain {
|
||||
Video => match &self.video_chain {
|
||||
Some(vc) => {
|
||||
if filter.starts_with(';') || filter.starts_with('[') {
|
||||
self.video_chain = Some(format!("{vc}{filter}"))
|
||||
@ -62,7 +72,6 @@ impl Filters {
|
||||
self.video_map = "[vout1]".to_string();
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -70,7 +79,7 @@ impl Filters {
|
||||
fn deinterlace(field_order: &Option<String>, chain: &mut Filters) {
|
||||
if let Some(order) = field_order {
|
||||
if order != "progressive" {
|
||||
chain.add_filter("yadif=0:-1:0", "video")
|
||||
chain.add_filter("yadif=0:-1:0", Video)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -91,34 +100,40 @@ fn pad(aspect: f64, chain: &mut Filters, v_stream: &ffprobe::Stream, config: &Pl
|
||||
"{scale}pad=max(iw\\,ih*({0}/{1})):ow/({0}/{1}):(ow-iw)/2:(oh-ih)/2",
|
||||
config.processing.width, config.processing.height
|
||||
),
|
||||
"video",
|
||||
Video,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn fps(fps: f64, chain: &mut Filters, config: &PlayoutConfig) {
|
||||
if fps != config.processing.fps {
|
||||
chain.add_filter(&format!("fps={}", config.processing.fps), "video")
|
||||
chain.add_filter(&format!("fps={}", config.processing.fps), Video)
|
||||
}
|
||||
}
|
||||
|
||||
fn scale(v_stream: &ffprobe::Stream, aspect: f64, chain: &mut Filters, config: &PlayoutConfig) {
|
||||
fn scale(
|
||||
width: Option<i64>,
|
||||
height: Option<i64>,
|
||||
aspect: f64,
|
||||
chain: &mut Filters,
|
||||
config: &PlayoutConfig,
|
||||
) {
|
||||
// width: i64, height: i64
|
||||
if let (Some(w), Some(h)) = (v_stream.width, v_stream.height) {
|
||||
if let (Some(w), Some(h)) = (width, height) {
|
||||
if w != config.processing.width || h != config.processing.height {
|
||||
chain.add_filter(
|
||||
&format!(
|
||||
"scale={}:{}",
|
||||
config.processing.width, config.processing.height
|
||||
),
|
||||
"video",
|
||||
Video,
|
||||
);
|
||||
} else {
|
||||
chain.add_filter("null", "video");
|
||||
chain.add_filter("null", Video);
|
||||
}
|
||||
|
||||
if !is_close(aspect, config.processing.aspect, 0.03) {
|
||||
chain.add_filter(&format!("setdar=dar={}", config.processing.aspect), "video")
|
||||
chain.add_filter(&format!("setdar=dar={}", config.processing.aspect), Video)
|
||||
}
|
||||
} else {
|
||||
chain.add_filter(
|
||||
@ -126,20 +141,20 @@ fn scale(v_stream: &ffprobe::Stream, aspect: f64, chain: &mut Filters, config: &
|
||||
"scale={}:{}",
|
||||
config.processing.width, config.processing.height
|
||||
),
|
||||
"video",
|
||||
Video,
|
||||
);
|
||||
chain.add_filter(&format!("setdar=dar={}", config.processing.aspect), "video")
|
||||
chain.add_filter(&format!("setdar=dar={}", config.processing.aspect), Video)
|
||||
}
|
||||
}
|
||||
|
||||
fn fade(node: &mut Media, chain: &mut Filters, codec_type: &str) {
|
||||
fn fade(node: &mut Media, chain: &mut Filters, codec_type: FilterType) {
|
||||
let mut t = "";
|
||||
|
||||
if codec_type == "audio" {
|
||||
if codec_type == Audio {
|
||||
t = "a"
|
||||
}
|
||||
|
||||
if node.seek > 0.0 {
|
||||
if node.seek > 0.0 || node.is_live == Some(true) {
|
||||
chain.add_filter(&format!("{t}fade=in:st=0:d=0.5"), codec_type)
|
||||
}
|
||||
|
||||
@ -171,7 +186,7 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) {
|
||||
logo_chain
|
||||
.push_str(format!("[l];[v][l]{}:shortest=1", config.processing.logo_filter).as_str());
|
||||
|
||||
chain.add_filter(&logo_chain, "video");
|
||||
chain.add_filter(&logo_chain, Video);
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,8 +194,8 @@ fn extend_video(node: &mut Media, chain: &mut Filters) {
|
||||
if let Some(video_duration) = node
|
||||
.probe
|
||||
.as_ref()
|
||||
.and_then(|p| p.video_streams.as_ref())
|
||||
.and_then(|v| v[0].duration.as_ref())
|
||||
.and_then(|p| p.video_streams.get(0))
|
||||
.and_then(|v| v.duration.as_ref())
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
{
|
||||
if node.out - node.seek > video_duration - node.seek + 0.1 && node.duration >= node.out {
|
||||
@ -189,7 +204,7 @@ fn extend_video(node: &mut Media, chain: &mut Filters) {
|
||||
"tpad=stop_mode=add:stop_duration={}",
|
||||
(node.out - node.seek) - (video_duration - node.seek)
|
||||
),
|
||||
"video",
|
||||
Video,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -205,9 +220,9 @@ fn add_text(
|
||||
if config.text.add_text
|
||||
&& (config.text.text_from_filename || config.out.mode.to_lowercase() == "hls")
|
||||
{
|
||||
let filter = v_drawtext::filter_node(config, Some(node), filter_chain, false);
|
||||
let filter = v_drawtext::filter_node(config, Some(node), filter_chain);
|
||||
|
||||
chain.add_filter(&filter, "video");
|
||||
chain.add_filter(&filter, Video);
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,9 +230,8 @@ fn add_audio(node: &mut Media, chain: &mut Filters) {
|
||||
if node
|
||||
.probe
|
||||
.as_ref()
|
||||
.and_then(|p| p.audio_streams.as_ref())
|
||||
.unwrap_or(&vec![])
|
||||
.is_empty()
|
||||
.and_then(|p| p.audio_streams.get(0))
|
||||
.is_none()
|
||||
&& !Path::new(&node.audio).is_file()
|
||||
{
|
||||
warn!("Clip <b><magenta>{}</></b> has no audio!", node.source);
|
||||
@ -225,7 +239,7 @@ fn add_audio(node: &mut Media, chain: &mut Filters) {
|
||||
"aevalsrc=0:channel_layout=stereo:duration={}:sample_rate=48000",
|
||||
node.out - node.seek
|
||||
);
|
||||
chain.add_filter(&audio, "audio");
|
||||
chain.add_filter(&audio, Audio);
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,34 +251,28 @@ fn extend_audio(node: &mut Media, chain: &mut Filters) {
|
||||
};
|
||||
|
||||
if let Some(audio_duration) = probe
|
||||
.and_then(|p| p.audio_streams)
|
||||
.and_then(|a| a[0].duration.clone())
|
||||
.as_ref()
|
||||
.and_then(|p| p.audio_streams.get(0))
|
||||
.and_then(|a| a.duration.clone())
|
||||
.and_then(|a| a.parse::<f64>().ok())
|
||||
{
|
||||
if node.out - node.seek > audio_duration - node.seek + 0.1 && node.duration >= node.out {
|
||||
chain.add_filter(&format!("apad=whole_dur={}", node.out - node.seek), "audio")
|
||||
chain.add_filter(&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: &PlayoutConfig) {
|
||||
if config.processing.add_loudnorm
|
||||
&& !node
|
||||
.probe
|
||||
.as_ref()
|
||||
.and_then(|p| p.audio_streams.as_ref())
|
||||
.unwrap_or(&vec![])
|
||||
.is_empty()
|
||||
{
|
||||
fn add_loudnorm(chain: &mut Filters, config: &PlayoutConfig) {
|
||||
if config.processing.add_loudnorm {
|
||||
let loud_filter = a_loudnorm::filter_node(config);
|
||||
chain.add_filter(&loud_filter, "audio");
|
||||
chain.add_filter(&loud_filter, Audio);
|
||||
}
|
||||
}
|
||||
|
||||
fn audio_volume(chain: &mut Filters, config: &PlayoutConfig) {
|
||||
if config.processing.volume != 1.0 {
|
||||
chain.add_filter(&format!("volume={}", config.processing.volume), "audio")
|
||||
chain.add_filter(&format!("volume={}", config.processing.volume), Audio)
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,26 +289,17 @@ fn aspect_calc(aspect_string: &Option<String>, config: &PlayoutConfig) -> f64 {
|
||||
source_aspect
|
||||
}
|
||||
|
||||
fn fps_calc(r_frame_rate: &str) -> f64 {
|
||||
let frame_rate_vec = r_frame_rate.split('/').collect::<Vec<&str>>();
|
||||
let rate: f64 = frame_rate_vec[0].parse().unwrap();
|
||||
let factor: f64 = frame_rate_vec[1].parse().unwrap();
|
||||
let fps: f64 = rate / factor;
|
||||
|
||||
fps
|
||||
}
|
||||
|
||||
/// This realtime filter is important for HLS output to stay in sync.
|
||||
fn realtime_filter(
|
||||
node: &mut Media,
|
||||
chain: &mut Filters,
|
||||
config: &PlayoutConfig,
|
||||
codec_type: &str,
|
||||
codec_type: FilterType,
|
||||
) {
|
||||
if config.general.generate.is_none() && &config.out.mode.to_lowercase() == "hls" {
|
||||
let mut t = "";
|
||||
|
||||
if codec_type == "audio" {
|
||||
if codec_type == Audio {
|
||||
t = "a"
|
||||
}
|
||||
|
||||
@ -323,6 +322,18 @@ fn realtime_filter(
|
||||
}
|
||||
}
|
||||
|
||||
fn custom(filter: &str, chain: &mut Filters) {
|
||||
let (video_filter, audio_filter) = custom_filter(filter);
|
||||
|
||||
if !video_filter.is_empty() {
|
||||
chain.add_filter(&video_filter, Video);
|
||||
}
|
||||
|
||||
if !audio_filter.is_empty() {
|
||||
chain.add_filter(&audio_filter, Audio);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter_chains(
|
||||
config: &PlayoutConfig,
|
||||
node: &mut Media,
|
||||
@ -331,37 +342,47 @@ pub fn filter_chains(
|
||||
let mut filters = Filters::new();
|
||||
|
||||
if let Some(probe) = node.probe.as_ref() {
|
||||
if probe.audio_streams.is_some() && !Path::new(&node.audio).is_file() {
|
||||
filters.audio_map = "0:a".to_string();
|
||||
if probe.audio_streams.get(0).is_none() || Path::new(&node.audio).is_file() {
|
||||
filters.audio_map = "1:a".to_string();
|
||||
}
|
||||
|
||||
if let Some(v_streams) = &probe.video_streams.as_ref() {
|
||||
let v_stream = &v_streams[0];
|
||||
|
||||
if let Some(v_stream) = &probe.video_streams.get(0) {
|
||||
let aspect = aspect_calc(&v_stream.display_aspect_ratio, config);
|
||||
let frame_per_sec = fps_calc(&v_stream.r_frame_rate);
|
||||
let frame_per_sec = fps_calc(&v_stream.r_frame_rate, 1.0);
|
||||
|
||||
deinterlace(&v_stream.field_order, &mut filters);
|
||||
pad(aspect, &mut filters, v_stream, config);
|
||||
fps(frame_per_sec, &mut filters, config);
|
||||
scale(v_stream, aspect, &mut filters, config);
|
||||
scale(
|
||||
v_stream.width,
|
||||
v_stream.height,
|
||||
aspect,
|
||||
&mut filters,
|
||||
config,
|
||||
);
|
||||
}
|
||||
|
||||
extend_video(node, &mut filters);
|
||||
|
||||
add_audio(node, &mut filters);
|
||||
extend_audio(node, &mut filters);
|
||||
} else {
|
||||
fps(0.0, &mut filters, config);
|
||||
scale(None, None, 1.0, &mut filters, config);
|
||||
}
|
||||
|
||||
add_text(node, &mut filters, config, filter_chain);
|
||||
fade(node, &mut filters, "video");
|
||||
fade(node, &mut filters, Video);
|
||||
overlay(node, &mut filters, config);
|
||||
realtime_filter(node, &mut filters, config, "video");
|
||||
realtime_filter(node, &mut filters, config, Video);
|
||||
|
||||
add_loudnorm(node, &mut filters, config);
|
||||
fade(node, &mut filters, "audio");
|
||||
add_loudnorm(&mut filters, config);
|
||||
fade(node, &mut filters, Audio);
|
||||
audio_volume(&mut filters, config);
|
||||
realtime_filter(node, &mut filters, config, "audio");
|
||||
realtime_filter(node, &mut filters, config, Audio);
|
||||
|
||||
custom(&config.processing.custom_filter, &mut filters);
|
||||
custom(&node.custom_filter, &mut filters);
|
||||
|
||||
let mut filter_cmd = vec![];
|
||||
let mut filter_str: String = String::new();
|
||||
|
@ -11,7 +11,6 @@ pub fn filter_node(
|
||||
config: &PlayoutConfig,
|
||||
node: Option<&Media>,
|
||||
filter_chain: &Arc<Mutex<Vec<String>>>,
|
||||
is_server: bool,
|
||||
) -> String {
|
||||
let mut filter = String::new();
|
||||
let mut font = String::new();
|
||||
@ -21,9 +20,10 @@ pub fn filter_node(
|
||||
font = format!(":fontfile='{}'", config.text.fontfile)
|
||||
}
|
||||
|
||||
let zmq_socket = match is_server {
|
||||
true => config.text.zmq_server_socket.clone(),
|
||||
false => config.text.zmq_stream_socket.clone(),
|
||||
let zmq_socket = match node.and_then(|n| n.is_live) {
|
||||
Some(true) => config.text.zmq_server_socket.clone(),
|
||||
Some(false) => config.text.zmq_stream_socket.clone(),
|
||||
None => config.text.zmq_stream_socket.clone(),
|
||||
};
|
||||
|
||||
// TODO: in Rust 1.64 use let_chains instead
|
||||
|
@ -1,5 +1,3 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::utils::PlayoutConfig;
|
||||
|
||||
/// Overlay Filter
|
||||
@ -8,14 +6,18 @@ use crate::utils::PlayoutConfig;
|
||||
pub fn filter_node(config: &PlayoutConfig, add_tail: bool) -> String {
|
||||
let mut logo_chain = String::new();
|
||||
|
||||
if config.processing.add_logo && Path::new(&config.processing.logo).is_file() {
|
||||
if !config.processing.add_logo {
|
||||
return logo_chain;
|
||||
}
|
||||
|
||||
if let Some(fps) = config.processing.logo_fps {
|
||||
let opacity = format!(
|
||||
"format=rgba,colorchannelmixer=aa={}",
|
||||
config.processing.logo_opacity
|
||||
);
|
||||
let logo_loop = "loop=loop=-1:size=1:start=0";
|
||||
let pts = format!("setpts=N/({fps}*TB)");
|
||||
logo_chain = format!(
|
||||
"null[v];movie={},{logo_loop},{opacity}",
|
||||
"null[v];movie={}:loop=0,{pts},{opacity}",
|
||||
config.processing.logo
|
||||
);
|
||||
|
||||
@ -24,7 +26,7 @@ pub fn filter_node(config: &PlayoutConfig, add_tail: bool) -> String {
|
||||
format!("[l];[v][l]{}:shortest=1", config.processing.logo_filter).as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
logo_chain
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use std::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shlex::split;
|
||||
|
||||
use crate::utils::{free_tcp_socket, home_dir, time_to_sec};
|
||||
use crate::utils::{fps_calc, free_tcp_socket, home_dir, time_to_sec, MediaProbe};
|
||||
use crate::vec_strings;
|
||||
|
||||
pub const DUMMY_LEN: f64 = 60.0;
|
||||
@ -89,6 +89,10 @@ pub struct Processing {
|
||||
pub fps: f64,
|
||||
pub add_logo: bool,
|
||||
pub logo: String,
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub logo_fps: Option<f64>,
|
||||
|
||||
pub logo_scale: String,
|
||||
pub logo_opacity: f32,
|
||||
pub logo_filter: String,
|
||||
@ -98,6 +102,8 @@ pub struct Processing {
|
||||
pub loud_tp: f32,
|
||||
pub loud_lra: f32,
|
||||
pub volume: f64,
|
||||
#[serde(default)]
|
||||
pub custom_filter: String,
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub settings: Option<Vec<String>>,
|
||||
@ -221,6 +227,19 @@ impl PlayoutConfig {
|
||||
config.playlist.length_sec = Some(86400.0);
|
||||
}
|
||||
|
||||
config.processing.logo_fps = None;
|
||||
|
||||
if Path::new(&config.processing.logo).is_file() {
|
||||
if let Some(v_stream) = MediaProbe::new(&config.processing.logo)
|
||||
.video_streams
|
||||
.get(0)
|
||||
{
|
||||
let fps = fps_calc(&v_stream.r_frame_rate, config.processing.fps);
|
||||
|
||||
config.processing.logo_fps = Some(fps);
|
||||
};
|
||||
}
|
||||
|
||||
// We set the decoder settings here, so we only define them ones.
|
||||
let mut settings = vec_strings![
|
||||
"-pix_fmt",
|
||||
|
@ -67,8 +67,8 @@ impl Default for ProcessControl {
|
||||
}
|
||||
|
||||
impl ProcessControl {
|
||||
pub fn kill(&mut self, proc: ProcessUnit) -> Result<(), String> {
|
||||
match proc {
|
||||
pub fn kill(&self, unit: ProcessUnit) -> Result<(), String> {
|
||||
match unit {
|
||||
Decoder => {
|
||||
if let Some(proc) = self.decoder_term.lock().unwrap().as_mut() {
|
||||
if let Err(e) = proc.kill() {
|
||||
@ -92,7 +92,7 @@ impl ProcessControl {
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = self.wait(proc) {
|
||||
if let Err(e) = self.wait(unit) {
|
||||
return Err(e);
|
||||
};
|
||||
|
||||
@ -101,8 +101,8 @@ impl ProcessControl {
|
||||
|
||||
/// Wait for process to proper close.
|
||||
/// This prevents orphaned/zombi processes in system
|
||||
pub fn wait(&mut self, proc: ProcessUnit) -> Result<(), String> {
|
||||
match proc {
|
||||
pub fn wait(&self, unit: ProcessUnit) -> Result<(), String> {
|
||||
match unit {
|
||||
Decoder => {
|
||||
if let Some(proc) = self.decoder_term.lock().unwrap().as_mut() {
|
||||
if let Err(e) = proc.wait() {
|
||||
@ -151,6 +151,12 @@ impl ProcessControl {
|
||||
}
|
||||
}
|
||||
|
||||
// impl Drop for ProcessControl {
|
||||
// fn drop(&mut self) {
|
||||
// self.kill_all()
|
||||
// }
|
||||
// }
|
||||
|
||||
/// Global player control, to get infos about current clip etc.
|
||||
#[derive(Clone)]
|
||||
pub struct PlayerControl {
|
||||
|
@ -112,9 +112,11 @@ fn loop_playlist(
|
||||
cmd: item.cmd.clone(),
|
||||
probe: item.probe.clone(),
|
||||
process: Some(true),
|
||||
is_live: Some(false),
|
||||
last_ad: Some(false),
|
||||
next_ad: Some(false),
|
||||
filter: Some(vec![]),
|
||||
custom_filter: String::new(),
|
||||
};
|
||||
|
||||
if begin < start_sec + length {
|
||||
|
@ -4,7 +4,7 @@ use std::{
|
||||
io::{BufRead, BufReader, Error},
|
||||
net::TcpListener,
|
||||
path::{Path, PathBuf},
|
||||
process::{ChildStderr, Command, Stdio},
|
||||
process::{exit, ChildStderr, Command, Stdio},
|
||||
sync::{Arc, Mutex},
|
||||
time::{self, UNIX_EPOCH},
|
||||
};
|
||||
@ -69,6 +69,9 @@ pub struct Media {
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub filter: Option<Vec<String>>,
|
||||
|
||||
#[serde(default)]
|
||||
pub custom_filter: String,
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub probe: Option<MediaProbe>,
|
||||
|
||||
@ -80,6 +83,9 @@ pub struct Media {
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub process: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub is_live: Option<bool>,
|
||||
}
|
||||
|
||||
impl Media {
|
||||
@ -110,10 +116,12 @@ impl Media {
|
||||
audio: String::new(),
|
||||
cmd: Some(vec!["-i".to_string(), src]),
|
||||
filter: Some(vec![]),
|
||||
custom_filter: String::new(),
|
||||
probe,
|
||||
last_ad: Some(false),
|
||||
next_ad: Some(false),
|
||||
process: Some(true),
|
||||
is_live: Some(false),
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,8 +174,8 @@ where
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct MediaProbe {
|
||||
pub format: Option<Format>,
|
||||
pub audio_streams: Option<Vec<Stream>>,
|
||||
pub video_streams: Option<Vec<Stream>>,
|
||||
pub audio_streams: Vec<Stream>,
|
||||
pub video_streams: Vec<Stream>,
|
||||
}
|
||||
|
||||
impl MediaProbe {
|
||||
@ -194,33 +202,36 @@ impl MediaProbe {
|
||||
|
||||
MediaProbe {
|
||||
format: Some(obj.format),
|
||||
audio_streams: if !a_stream.is_empty() {
|
||||
Some(a_stream)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
video_streams: if !v_stream.is_empty() {
|
||||
Some(v_stream)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
audio_streams: a_stream,
|
||||
video_streams: v_stream,
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Can't read source <b><magenta>{input}</></b> with ffprobe, source not exists or damaged! Error is: {e:?}"
|
||||
"Can't read source <b><magenta>{input}</></b> with ffprobe, source not exists or damaged! Error in: {e:?}"
|
||||
);
|
||||
|
||||
MediaProbe {
|
||||
format: None,
|
||||
audio_streams: None,
|
||||
video_streams: None,
|
||||
audio_streams: vec![],
|
||||
video_streams: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate fps from rae/factor string
|
||||
pub fn fps_calc(r_frame_rate: &str, default: f64) -> f64 {
|
||||
let mut fps = default;
|
||||
|
||||
if let Some((r, f)) = r_frame_rate.split_once('/') {
|
||||
fps = r.parse::<f64>().unwrap_or(1.0) / f.parse::<f64>().unwrap_or(1.0);
|
||||
}
|
||||
|
||||
fps
|
||||
}
|
||||
|
||||
/// Covert JSON string to ffmpeg filter command.
|
||||
pub fn get_filter_from_json(raw_text: String) -> String {
|
||||
let re1 = Regex::new(r#""|}|\{"#).unwrap();
|
||||
@ -468,11 +479,12 @@ pub fn seek_and_length(node: &Media) -> Vec<String> {
|
||||
|
||||
source_cmd.append(&mut vec_strings!["-i", node.audio.clone()]);
|
||||
|
||||
if audio_probe
|
||||
.audio_streams
|
||||
.and_then(|a| a[0].duration.clone())
|
||||
.and_then(|d| d.parse::<f64>().ok())
|
||||
> Some(node.out - node.seek)
|
||||
if !audio_probe.audio_streams.is_empty()
|
||||
&& audio_probe.audio_streams[0]
|
||||
.duration
|
||||
.clone()
|
||||
.and_then(|d| d.parse::<f64>().ok())
|
||||
> Some(node.out - node.seek)
|
||||
{
|
||||
cut_audio = true;
|
||||
}
|
||||
@ -593,7 +605,7 @@ pub fn is_remote(path: &str) -> bool {
|
||||
///
|
||||
/// Check if input is a remote source, or from storage and see if it exists.
|
||||
pub fn valid_source(source: &str) -> bool {
|
||||
if is_remote(source) && MediaProbe::new(source).video_streams.is_some() {
|
||||
if is_remote(source) && !MediaProbe::new(source).video_streams.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -652,7 +664,11 @@ pub fn format_log_line(line: String, level: &str) -> String {
|
||||
|
||||
/// Read ffmpeg stderr decoder and encoder 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,
|
||||
mut proc_control: ProcessControl,
|
||||
) -> Result<(), Error> {
|
||||
for line in buffer.lines() {
|
||||
let line = line?;
|
||||
|
||||
@ -666,16 +682,20 @@ pub fn stderr_reader(buffer: BufReader<ChildStderr>, suffix: &str) -> Result<(),
|
||||
"<bright black>[{suffix}]</> {}",
|
||||
format_log_line(line, "warning")
|
||||
)
|
||||
} else if line.contains("[error]") {
|
||||
} else if line.contains("[error]") || line.contains("[fatal]") {
|
||||
error!(
|
||||
"<bright black>[{suffix}]</> {}",
|
||||
format_log_line(line, "error")
|
||||
)
|
||||
} else if line.contains("[fatal]") {
|
||||
error!(
|
||||
"<bright black>[{suffix}]</> {}",
|
||||
format_log_line(line, "fatal")
|
||||
)
|
||||
line.replace("[error]", "").replace("[fatal]", "")
|
||||
);
|
||||
|
||||
if line.contains("Invalid argument")
|
||||
|| line.contains("Numerical result")
|
||||
|| line.contains("No such file or directory")
|
||||
|| line.contains("Error initializing complex filters")
|
||||
{
|
||||
proc_control.kill_all();
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user