Merge pull request #186 from jb-alvarado/master

support animated logo, add custom filter paramter in config and playlist
This commit is contained in:
jb-alvarado 2022-08-31 21:42:39 +02:00 committed by GitHub
commit 775ca7750b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 415 additions and 298 deletions

113
Cargo.lock generated
View File

@ -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",

View File

@ -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,

View File

@ -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

View File

@ -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
View 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.

View File

@ -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]

View File

@ -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;

View File

@ -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];
}

View File

@ -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:?}")
};

View File

@ -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

View File

@ -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];
}

View File

@ -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];

View File

@ -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

View File

@ -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]

View 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)
}

View File

@ -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(),
]
}

View File

@ -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();

View File

@ -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

View File

@ -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
}

View File

@ -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",

View File

@ -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 {

View File

@ -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 {

View File

@ -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);
}
}
}