Merge pull request #190 from jb-alvarado/master
validate processing settings and filter/custom filter
This commit is contained in:
commit
3b6dd6c695
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -941,7 +941,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ffplayout"
|
||||
version = "0.15.0"
|
||||
version = "0.15.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
@ -960,7 +960,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ffplayout-api"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"actix-files",
|
||||
"actix-multipart",
|
||||
@ -990,7 +990,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ffplayout-lib"
|
||||
version = "0.15.0"
|
||||
version = "0.15.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"crossbeam-channel",
|
||||
|
@ -9,13 +9,13 @@ It is possible to apply only video or audio filters, or both. For a better under
|
||||
#### Apply Gaussian blur and volume filter:
|
||||
|
||||
```YAML
|
||||
custom_filter: 'gblur=5[c_v_out];volume=0.5[c_a_out]'
|
||||
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]'
|
||||
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.
|
||||
@ -23,7 +23,7 @@ Pay attention to the filter prefix `[v_in];`, this is necessary to get the outpu
|
||||
#### Paint effect
|
||||
|
||||
```YAML
|
||||
custom_filter: edgedetect=mode=colormix:high=0[c_v_out]
|
||||
custom_filter: "edgedetect=mode=colormix:high=0[c_v_out]"
|
||||
```
|
||||
|
||||
### Where the filters applied in stream mode
|
||||
@ -50,7 +50,7 @@ The **custom filter** from **config -> processing** and from **playlist** got ap
|
||||
This example takes a image and a animated mov clip with alpha and overlays them two times on different positions in time:
|
||||
|
||||
```YAML
|
||||
custom_filter: '[v_in];movie=image_input.png:s=v,loop=loop=250.0:size=1:start=0,scale=1024:576,split=2[lower_1_out_1][lower_1_out_2];[lower_1_out_1]fifo,fade=in:duration=0.5:alpha=1,fade=out:start_time=9.5:duration=0.5:alpha=1,setpts=PTS+5.0/TB[fade_1];[v_in][fade_1]overlay=enable=between(t\,5.0\,15.0)[base_1];[lower_1_out_2]fifo,fade=in:duration=0.5:alpha=1,fade=out:start_time=9.5:duration=0.5:alpha=1,setpts=PTS+30.0/TB[fade_2];[base_1][fade_2]overlay=enable=between(t\,30.0\,40.0)[base_2];movie=animated_input.mov:s=v,scale=1024:576,split=2[lower_2_out_1][lower_2_out_2];[lower_2_out_1]fifo,setpts=PTS+20.0/TB[layer_1];[base_2][layer_1]overlay=repeatlast=0[base_3];[lower_2_out_2]fifo,setpts=PTS+50.0/TB[layer_2];[base_3][layer_2]overlay=repeatlast=0[c_v_out]'
|
||||
custom_filter: "[v_in];movie=image_input.png:s=v,loop=loop=250.0:size=1:start=0,scale=1024:576,split=2[lower_1_out_1][lower_1_out_2];[lower_1_out_1]fifo,fade=in:duration=0.5:alpha=1,fade=out:start_time=9.5:duration=0.5:alpha=1,setpts=PTS+5.0/TB[fade_1];[v_in][fade_1]overlay=enable=between(t\,5.0\,15.0)[base_1];[lower_1_out_2]fifo,fade=in:duration=0.5:alpha=1,fade=out:start_time=9.5:duration=0.5:alpha=1,setpts=PTS+30.0/TB[fade_2];[base_1][fade_2]overlay=enable=between(t\,30.0\,40.0)[base_2];movie=animated_input.mov:s=v,scale=1024:576,split=2[lower_2_out_1][lower_2_out_2];[lower_2_out_1]fifo,setpts=PTS+20.0/TB[layer_1];[base_2][layer_1]overlay=repeatlast=0[base_3];[lower_2_out_2]fifo,setpts=PTS+50.0/TB[layer_2];[base_3][layer_2]overlay=repeatlast=0[c_v_out]"
|
||||
```
|
||||
|
||||
And here are the explanation for each filter:
|
||||
|
@ -4,7 +4,7 @@ description = "Rest API for ffplayout"
|
||||
license = "GPL-3.0"
|
||||
authors = ["Jonathan Baecker jonbae77@gmail.com"]
|
||||
readme = "README.md"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
@ -5,7 +5,6 @@ use reqwest::{
|
||||
Client, Response,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use simplelog::*;
|
||||
|
||||
use crate::utils::{errors::ServiceError, handles::db_get_channel, playout_config};
|
||||
use ffplayout_lib::vec_strings;
|
||||
@ -146,10 +145,7 @@ where
|
||||
.await
|
||||
{
|
||||
Ok(result) => Ok(result),
|
||||
Err(e) => {
|
||||
error!("{e:?}");
|
||||
Err(ServiceError::ServiceUnavailable(e.to_string()))
|
||||
}
|
||||
Err(e) => Err(ServiceError::ServiceUnavailable(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.15.0"
|
||||
version = "0.15.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 37ccb38b91b34095d06673952a913be3cf79eb0a
|
||||
Subproject commit a1b203ebcfa0b3d9109ac049d332c2a45ad19e70
|
@ -4,7 +4,7 @@ description = "Library for ffplayout"
|
||||
license = "GPL-3.0"
|
||||
authors = ["Jonathan Baecker jonbae77@gmail.com"]
|
||||
readme = "README.md"
|
||||
version = "0.15.0"
|
||||
version = "0.15.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
@ -173,11 +173,11 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) {
|
||||
{
|
||||
let mut logo_chain = v_overlay::filter_node(config, false);
|
||||
|
||||
if node.last_ad.unwrap() {
|
||||
if node.last_ad.unwrap_or(false) {
|
||||
logo_chain.push_str(",fade=in:st=0:d=1.0:alpha=1")
|
||||
}
|
||||
|
||||
if node.next_ad.unwrap() {
|
||||
if node.next_ad.unwrap_or(false) {
|
||||
logo_chain.push_str(
|
||||
format!(",fade=out:st={}:d=1.0:alpha=1", node.out - node.seek - 1.0).as_str(),
|
||||
)
|
||||
|
@ -141,7 +141,7 @@ pub fn generate_playlist(
|
||||
|
||||
length += duration;
|
||||
} else if filler_length > 0.0 && filler_length > total_length - length {
|
||||
filler.out = filler_length - (total_length - length);
|
||||
filler.out = total_length - length;
|
||||
playlist.program.push(filler);
|
||||
|
||||
break;
|
||||
|
@ -1,11 +1,106 @@
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Error, ErrorKind},
|
||||
process::{Command, Stdio},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
use simplelog::*;
|
||||
|
||||
use crate::utils::{sec_to_time, valid_source, JsonPlaylist, MediaProbe, PlayoutConfig};
|
||||
use crate::utils::{
|
||||
format_log_line, loop_image, sec_to_time, seek_and_length, valid_source, vec_strings,
|
||||
JsonPlaylist, Media, PlayoutConfig, IMAGE_FORMAT,
|
||||
};
|
||||
|
||||
/// check if ffmpeg can read the file and apply filter to it.
|
||||
fn check_media(
|
||||
mut node: Media,
|
||||
pos: usize,
|
||||
begin: f64,
|
||||
config: &PlayoutConfig,
|
||||
) -> Result<(), Error> {
|
||||
let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", "level+error"];
|
||||
let mut error_list = vec![];
|
||||
|
||||
node.add_probe();
|
||||
|
||||
if node.probe.clone().and_then(|p| p.format).is_none() {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"No Metadata at position <yellow>{pos}</> {}, from file <b><magenta>\"{}\"</></b>",
|
||||
sec_to_time(begin),
|
||||
node.source
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// take care, that no seek and length command is added.
|
||||
node.seek = 0.0;
|
||||
node.out = node.duration;
|
||||
|
||||
if node
|
||||
.source
|
||||
.rsplit_once('.')
|
||||
.map(|(_, e)| e.to_lowercase())
|
||||
.filter(|c| IMAGE_FORMAT.contains(&c.as_str()))
|
||||
.is_some()
|
||||
{
|
||||
node.cmd = Some(loop_image(&node));
|
||||
} else {
|
||||
node.cmd = Some(seek_and_length(&node));
|
||||
}
|
||||
|
||||
node.add_filter(config, &Arc::new(Mutex::new(vec![])));
|
||||
|
||||
enc_cmd.append(&mut node.cmd.unwrap_or_default());
|
||||
enc_cmd.append(&mut node.filter.unwrap_or_default());
|
||||
enc_cmd.append(&mut vec_strings!["-t", "0.15", "-f", "null", "-"]);
|
||||
|
||||
let mut enc_proc = match Command::new("ffmpeg")
|
||||
.args(enc_cmd.clone())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Err(e) => return Err(e),
|
||||
Ok(proc) => proc,
|
||||
};
|
||||
|
||||
let enc_err = BufReader::new(enc_proc.stderr.take().unwrap());
|
||||
|
||||
for line in enc_err.lines() {
|
||||
let line = line?;
|
||||
|
||||
if line.contains("[error]") {
|
||||
let log_line = format_log_line(line, "error");
|
||||
|
||||
if !error_list.contains(&log_line) {
|
||||
error_list.push(log_line);
|
||||
}
|
||||
} else if line.contains("[fatal]") {
|
||||
let log_line = format_log_line(line, "fatal");
|
||||
|
||||
if !error_list.contains(&log_line) {
|
||||
error_list.push(log_line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !error_list.is_empty() {
|
||||
error!(
|
||||
"<bright black>[Validator]</> Compressing error on position <yellow>{pos}</> {}: <b><magenta>{}</></b>:\n{}",
|
||||
sec_to_time(begin),
|
||||
node.source,
|
||||
error_list.join("\n")
|
||||
)
|
||||
}
|
||||
|
||||
error_list.clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate a given playlist, to check if:
|
||||
///
|
||||
@ -25,26 +120,22 @@ pub fn validate_playlist(
|
||||
|
||||
length += begin;
|
||||
|
||||
debug!("validate playlist from: <yellow>{date}</>");
|
||||
debug!("Validate playlist from: <yellow>{date}</>");
|
||||
|
||||
for item in playlist.program.iter() {
|
||||
for (index, item) in playlist.program.iter().enumerate() {
|
||||
if is_terminated.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
if valid_source(&item.source) {
|
||||
let probe = MediaProbe::new(&item.source);
|
||||
let pos = index + 1;
|
||||
|
||||
if probe.format.is_none() {
|
||||
error!(
|
||||
"No Metadata at <yellow>{}</>, from file <b><magenta>\"{}\"</></b>",
|
||||
sec_to_time(begin),
|
||||
item.source
|
||||
);
|
||||
}
|
||||
if valid_source(&item.source) {
|
||||
if let Err(e) = check_media(item.clone(), pos, begin, &config) {
|
||||
error!("{e}");
|
||||
};
|
||||
} else {
|
||||
error!(
|
||||
"Source on position <yellow>{}</> not exists: <b><magenta>\"{}\"</></b>",
|
||||
"Source on position <yellow>{pos}</> {} not exists: <b><magenta>\"{}\"</></b>",
|
||||
sec_to_time(begin),
|
||||
item.source
|
||||
);
|
||||
@ -59,4 +150,6 @@ pub fn validate_playlist(
|
||||
sec_to_time(length - begin),
|
||||
);
|
||||
}
|
||||
|
||||
debug!("Validation done...");
|
||||
}
|
||||
|
@ -55,12 +55,20 @@ pub struct Media {
|
||||
pub out: f64,
|
||||
pub duration: f64,
|
||||
|
||||
#[serde(default, deserialize_with = "null_string")]
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "null_string",
|
||||
skip_serializing_if = "is_empty_string"
|
||||
)]
|
||||
pub category: String,
|
||||
#[serde(deserialize_with = "null_string")]
|
||||
pub source: String,
|
||||
|
||||
#[serde(default, deserialize_with = "null_string")]
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "null_string",
|
||||
skip_serializing_if = "is_empty_string"
|
||||
)]
|
||||
pub audio: String,
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
@ -69,7 +77,7 @@ pub struct Media {
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub filter: Option<Vec<String>>,
|
||||
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "is_empty_string")]
|
||||
pub custom_filter: String,
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
@ -158,6 +166,8 @@ impl PartialEq for Media {
|
||||
&& self.duration == other.duration
|
||||
&& self.source == other.source
|
||||
&& self.category == other.category
|
||||
&& self.audio == other.audio
|
||||
&& self.custom_filter == other.custom_filter
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,6 +180,11 @@ where
|
||||
Deserialize::deserialize(d).map(|x: Option<_>| x.unwrap_or_default())
|
||||
}
|
||||
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_empty_string(st: &String) -> bool {
|
||||
*st == String::new()
|
||||
}
|
||||
|
||||
/// We use the ffprobe crate, but we map the metadata to our needs.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct MediaProbe {
|
||||
@ -463,12 +478,15 @@ pub fn seek_and_length(node: &Media) -> Vec<String> {
|
||||
source_cmd.append(&mut vec_strings!["-ss", node.seek])
|
||||
}
|
||||
|
||||
source_cmd.append(&mut vec_strings![
|
||||
"-ignore_chapters",
|
||||
"1",
|
||||
"-i",
|
||||
node.source.clone()
|
||||
]);
|
||||
if file_extension(Path::new(&node.source))
|
||||
.unwrap_or_default()
|
||||
.to_lowercase()
|
||||
== "mp4"
|
||||
{
|
||||
source_cmd.append(&mut vec_strings!["-ignore_chapters", "1"]);
|
||||
}
|
||||
|
||||
source_cmd.append(&mut vec_strings!["-i", node.source.clone()]);
|
||||
|
||||
if Path::new(&node.audio).is_file() {
|
||||
let audio_probe = MediaProbe::new(&node.audio);
|
||||
|
Loading…
Reference in New Issue
Block a user