Merge pull request #383 from jb-alvarado/master

v0.20.0
This commit is contained in:
jb-alvarado 2023-08-07 17:48:15 +00:00 committed by GitHub
commit ccce51e578
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 742 additions and 341 deletions

8
Cargo.lock generated
View File

@ -1065,7 +1065,7 @@ dependencies = [
[[package]]
name = "ffplayout"
version = "0.19.0"
version = "0.20.0"
dependencies = [
"chrono",
"clap",
@ -1086,7 +1086,7 @@ dependencies = [
[[package]]
name = "ffplayout-api"
version = "0.19.0"
version = "0.20.0"
dependencies = [
"actix-files",
"actix-multipart",
@ -1119,7 +1119,7 @@ dependencies = [
[[package]]
name = "ffplayout-lib"
version = "0.19.0"
version = "0.20.0"
dependencies = [
"chrono",
"crossbeam-channel",
@ -3098,7 +3098,7 @@ dependencies = [
[[package]]
name = "tests"
version = "0.19.0"
version = "0.20.0"
dependencies = [
"chrono",
"crossbeam-channel",

View File

@ -3,7 +3,7 @@ members = ["ffplayout-api", "ffplayout-engine", "lib", "tests"]
default-members = ["ffplayout-api", "ffplayout-engine", "tests"]
[workspace.package]
version = "0.19.0"
version = "0.20.0"
license = "GPL-3.0"
repository = "https://github.com/ffplayout/ffplayout"
authors = ["Jonathan Baecker <jonbae77@gmail.com>"]

View File

@ -15,7 +15,7 @@ Check the [releases](https://github.com/ffplayout/ffplayout/releases/latest) for
- have all values in a separate config file
- dynamic playlist
- replace missing playlist or clip with a dummy clip
- replace missing playlist or clip with single filler or multiple fillers from folder, if no filler exists, create dummy clip
- playing clips in [watched](/docs/folder_mode.md) folder mode
- send emails with error message
- overlay a logo
@ -23,7 +23,7 @@ Check the [releases](https://github.com/ffplayout/ffplayout/releases/latest) for
- loop playlist infinitely
- [remote source](/docs/remote_source.md)
- trim and fade the last clip, to get full 24 hours
- when playlist is not 24 hours long, loop filler clip until time is full
- when playlist is not 24 hours long, loop fillers until time is full
- set custom day start, so you can have playlist for example: from 6am to 6am, instate of 0am to 12pm
- normal system requirements and no special tools
- no GPU power is needed
@ -47,6 +47,7 @@ Check the [releases](https://github.com/ffplayout/ffplayout/releases/latest) for
- image source (will loop until out duration is reached)
- extra audio source, has priority over audio from video (experimental *)
- [multiple audio tracks](/docs/multi_audio.md) (experimental *)
- [Stream Copy](/docs/stream_copy.md) mode (experimental *)
- [custom filters](/docs/custom_filters.md) globally in config, or in playlist for specific clips
- import playlist from text or m3u file, with CLI or frontend
- audio only, for radio mode (experimental *)
@ -63,7 +64,7 @@ ffpapi serves the [frontend](https://github.com/ffplayout/ffplayout-frontend) an
### Requirements
- RAM and CPU depends on video resolution, minimum 4 threads and 3GB RAM for 720p are recommend
- **ffmpeg** v4.2+ and **ffprobe** (**ffplay** if you want to play on desktop)
- **ffmpeg** v5.0+ and **ffprobe** (**ffplay** if you want to play on desktop)
- if you want to overlay text, ffmpeg needs to have **libzmq**
### Install

View File

@ -57,6 +57,8 @@ processing:
[c_v_out] for video filter, and [c_a_out] for audio filter.
mode: playlist
audio_only: false
copy_audio: false
copy_video: false
width: 1024
height: 576
aspect: 1.778
@ -67,6 +69,7 @@ processing:
logo_opacity: 0.7
logo_filter: overlay=W-w-12:12
audio_tracks: 1
audio_track_index: -1
audio_channels: 2
volume: 1
custom_filter:
@ -95,11 +98,12 @@ playlist:
infinit: false
storage:
help_text: Play ordered or randomly files from path. 'filler_clip' is for fill
the end to reach 24 hours, it will loop when is necessary. 'extensions' search
help_text: >
'filler' is for playing instead of a missing file or fill the end to reach 24
hours, can be a file or folder, it will loop when is necessary. 'extensions' search
only files with this extension. Set 'shuffle' to 'true' to pick files randomly.
path: "/var/lib/ffplayout/tv-media"
filler_clip: "/var/lib/ffplayout/tv-media/filler/filler.mp4"
filler: "/var/lib/ffplayout/tv-media/filler/filler.mp4"
extensions:
- "mp4"
- "mkv"
@ -117,6 +121,13 @@ text:
style: "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4"
regex: ^.+[/\\](.*)(.mp4|.mkv)$
task:
help_text: Run an external program with a given media object. The media object is in json format
and contains all the information about the current clip. The external program can be a script
or a binary, but should only run for a short time.
enable: false
path:
out:
help_text: The final playout compression. Set the settings to your needs. 'mode'
has the options 'desktop', 'hls', 'null', 'stream'. Use 'stream' and adjust

View File

@ -40,3 +40,7 @@ Use of remote sources, like https://example.org/video.mp4
### **[ffplayout API](/docs/api.md)**
Control the engine, playlist and config with a ~REST API
### **[Stream Copy](/docs/stream_copy.md)**
Copy audio and or video stream

10
docs/stream_copy.md Normal file
View File

@ -0,0 +1,10 @@
### Stream Copy
ffplayout supports a stream copy mode since v0.20.0. A separate copy mode for video and audio is possible. This mode uses less CPU and RAM, but has some drawbacks:
- All files must have exactly the same resolution, framerate, color depth, audio channels and kHz.
- All files must use the same codecs and settings.
- The video and audio lines of a file must be the same length.
- The codecs and A/V settings must be supported by mpegts and the output destination.
**This mode is experimental and will not have the same stability as the stream mode.**

View File

@ -278,7 +278,7 @@ pub async fn update_preset(
) -> Result<SqliteQueryResult, sqlx::Error> {
let query =
"UPDATE presets SET name = $1, text = $2, x = $3, y = $4, fontsize = $5, line_spacing = $6,
fontcolor = $7, alpha = $8, box = $9, boxcolor = $10, boxborderw = 11 WHERE id = $12";
fontcolor = $7, alpha = $8, box = $9, boxcolor = $10, boxborderw = $11 WHERE id = $12";
sqlx::query(query)
.bind(preset.name)

View File

@ -1,4 +1,8 @@
use serde::{Deserialize, Serialize};
use regex::Regex;
use serde::{
de::{self, Visitor},
Deserialize, Serialize,
};
#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)]
pub struct User {
@ -49,15 +53,55 @@ pub struct TextPreset {
pub text: String,
pub x: String,
pub y: String,
#[serde(deserialize_with = "deserialize_number_or_string")]
pub fontsize: String,
#[serde(deserialize_with = "deserialize_number_or_string")]
pub line_spacing: String,
pub fontcolor: String,
pub r#box: String,
pub boxcolor: String,
#[serde(deserialize_with = "deserialize_number_or_string")]
pub boxborderw: String,
#[serde(deserialize_with = "deserialize_number_or_string")]
pub alpha: String,
}
/// Deserialize number or string
pub fn deserialize_number_or_string<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
struct StringOrNumberVisitor;
impl<'de> Visitor<'de> for StringOrNumberVisitor {
type Value = String;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or a number")
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
let re = Regex::new(r"0,([0-9]+)").unwrap();
let clean_string = re.replace_all(value, "0.$1").to_string();
Ok(clean_string)
}
fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
Ok(value.to_string())
}
fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
Ok(value.to_string())
}
fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
Ok(value.to_string())
}
}
deserializer.deserialize_any(StringOrNumberVisitor)
}
#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)]
pub struct Channel {
#[serde(skip_deserializing)]

View File

@ -17,7 +17,7 @@ use notify::{
use notify_debouncer_full::new_debouncer;
use simplelog::*;
use ffplayout_lib::utils::{include_file, Media, PlayoutConfig};
use ffplayout_lib::utils::{include_file_extension, Media, PlayoutConfig};
/// Create a watcher, which monitor file changes.
/// When a change is register, update the current file list.
@ -52,7 +52,7 @@ pub fn watchman(
Create(CreateKind::File) | Modify(ModifyKind::Name(RenameMode::To)) => {
let new_path = &event.paths[0];
if new_path.is_file() && include_file(config.clone(), new_path) {
if new_path.is_file() && include_file_extension(&config, new_path) {
let index = sources.lock().unwrap().len();
let media = Media::new(index, &new_path.to_string_lossy(), false);
@ -63,7 +63,7 @@ pub fn watchman(
Remove(RemoveKind::File) | Modify(ModifyKind::Name(RenameMode::From)) => {
let old_path = &event.paths[0];
if !old_path.is_file() && include_file(config.clone(), old_path) {
if !old_path.is_file() && include_file_extension(&config, old_path) {
sources
.lock()
.unwrap()
@ -83,7 +83,7 @@ pub fn watchman(
let media = Media::new(index, &new_path.to_string_lossy(), false);
media_list[index] = media;
info!("Move file: <b><magenta>{old_path:?}</></b> to <b><magenta>{new_path:?}</></b>");
} else if include_file(config.clone(), new_path) {
} else if include_file_extension(&config, new_path) {
let index = media_list.len();
let media = Media::new(index, &new_path.to_string_lossy(), false);

View File

@ -1,8 +1,5 @@
use std::{
sync::{
atomic::{AtomicBool, AtomicUsize},
Arc, Mutex,
},
sync::{atomic::AtomicBool, Arc},
thread,
};
@ -18,13 +15,12 @@ pub use folder::watchman;
pub use ingest::ingest_server;
pub use playlist::CurrentProgram;
use ffplayout_lib::utils::folder::FolderSource;
use ffplayout_lib::utils::{controller::PlayerControl, folder::FolderSource};
/// Create a source iterator from playlist, or from folder.
pub fn source_generator(
config: PlayoutConfig,
current_list: Arc<Mutex<Vec<Media>>>,
index: Arc<AtomicUsize>,
player_control: &PlayerControl,
playout_stat: PlayoutStatus,
is_terminated: Arc<AtomicBool>,
) -> Box<dyn Iterator<Item = Media>> {
@ -37,8 +33,8 @@ pub fn source_generator(
);
let config_clone = config.clone();
let folder_source = FolderSource::new(&config, playout_stat.chain, current_list, index);
let node_clone = folder_source.nodes.clone();
let folder_source = FolderSource::new(&config, playout_stat.chain, player_control);
let node_clone = folder_source.player_control.current_list.clone();
// Spawn a thread to monitor folder for file changes.
thread::spawn(move || watchman(config_clone, is_terminated.clone(), node_clone));
@ -47,8 +43,7 @@ pub fn source_generator(
}
Playlist => {
info!("Playout in playlist mode");
let program =
CurrentProgram::new(&config, playout_stat, is_terminated, current_list, index);
let program = CurrentProgram::new(&config, playout_stat, is_terminated, player_control);
Box::new(program) as Box<dyn Iterator<Item = Media>>
}

View File

@ -2,7 +2,7 @@ use std::{
fs,
path::Path,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
};
@ -11,9 +11,9 @@ use serde_json::json;
use simplelog::*;
use ffplayout_lib::utils::{
check_sync, gen_dummy, get_delta, get_sec, is_close, is_remote, json_serializer::read_json,
loop_filler, loop_image, modified_time, seek_and_length, valid_source, Media, MediaProbe,
PlayoutConfig, PlayoutStatus, DUMMY_LEN, IMAGE_FORMAT,
check_sync, controller::PlayerControl, gen_dummy, get_delta, get_sec, is_close, is_remote,
json_serializer::read_json, loop_filler, loop_image, modified_time, seek_and_length,
valid_source, Media, MediaProbe, PlayoutConfig, PlayoutStatus, DUMMY_LEN, IMAGE_FORMAT,
};
/// Struct for current playlist.
@ -26,20 +26,19 @@ pub struct CurrentProgram {
json_mod: Option<String>,
json_path: Option<String>,
json_date: String,
pub nodes: Arc<Mutex<Vec<Media>>>,
player_control: PlayerControl,
current_node: Media,
index: Arc<AtomicUsize>,
is_terminated: Arc<AtomicBool>,
playout_stat: PlayoutStatus,
}
/// Prepare a playlist iterator.
impl CurrentProgram {
pub fn new(
config: &PlayoutConfig,
playout_stat: PlayoutStatus,
is_terminated: Arc<AtomicBool>,
current_list: Arc<Mutex<Vec<Media>>>,
global_index: Arc<AtomicUsize>,
player_control: &PlayerControl,
) -> Self {
let json = read_json(config, None, is_terminated.clone(), true, 0.0);
@ -47,7 +46,7 @@ impl CurrentProgram {
info!("Read Playlist: <b><magenta>{}</></b>", file);
}
*current_list.lock().unwrap() = json.program;
*player_control.current_list.lock().unwrap() = json.program;
*playout_stat.current_date.lock().unwrap() = json.date.clone();
if *playout_stat.date.lock().unwrap() != json.date {
@ -68,9 +67,8 @@ impl CurrentProgram {
json_mod: json.modified,
json_path: json.current_file,
json_date: json.date,
nodes: current_list,
player_control: player_control.clone(),
current_node: Media::new(0, "", false),
index: global_index,
is_terminated,
playout_stat,
}
@ -79,6 +77,7 @@ impl CurrentProgram {
// Check if playlist file got updated, and when yes we reload it and setup everything in place.
fn check_update(&mut self, seek: bool) {
if self.json_path.is_none() {
// If the playlist was missing, we check here to see if it came back.
let json = read_json(&self.config, None, self.is_terminated.clone(), seek, 0.0);
if let Some(file) = &json.current_file {
@ -87,10 +86,11 @@ impl CurrentProgram {
self.json_path = json.current_file;
self.json_mod = json.modified;
*self.nodes.lock().unwrap() = json.program;
*self.player_control.current_list.lock().unwrap() = json.program;
} else if Path::new(&self.json_path.clone().unwrap()).is_file()
|| is_remote(&self.json_path.clone().unwrap())
{
// If the playlist exists, we check here if it has been modified.
let mod_time = modified_time(&self.json_path.clone().unwrap());
if self.json_mod != mod_time {
@ -109,25 +109,26 @@ impl CurrentProgram {
);
self.json_mod = json.modified;
*self.nodes.lock().unwrap() = json.program;
*self.player_control.current_list.lock().unwrap() = json.program;
self.playout_stat.list_init.store(true, Ordering::SeqCst);
}
} else {
// If the playlist disappears after normal run, we end up here.
trace!("check_update, missing playlist");
error!(
"Playlist <b><magenta>{}</></b> not exists!",
"Playlist <b><magenta>{}</></b> not exist!",
self.json_path.clone().unwrap()
);
let mut media = Media::new(0, "", false);
media.begin = Some(get_sec());
media.duration = DUMMY_LEN;
media.out = DUMMY_LEN;
let media = Media::new(0, "", false);
self.json_mod = None;
self.json_path = None;
*self.nodes.lock().unwrap() = vec![media.clone()];
self.current_node = media;
self.current_node = media.clone();
self.playout_stat.list_init.store(true, Ordering::SeqCst);
self.index.store(0, Ordering::SeqCst);
self.player_control.current_index.store(0, Ordering::SeqCst);
*self.player_control.current_list.lock().unwrap() = vec![media];
}
}
@ -143,12 +144,16 @@ impl CurrentProgram {
duration = self.current_node.duration
}
let mut next_start = self.current_node.begin.unwrap() - start_sec + duration + delta;
let mut next_start =
self.current_node.begin.unwrap_or_default() - start_sec + duration + delta;
if self.index.load(Ordering::SeqCst) == self.nodes.lock().unwrap().len() - 1 {
if self.player_control.current_index.load(Ordering::SeqCst)
== self.player_control.current_list.lock().unwrap().len() - 1
{
next_start += self.config.general.stop_threshold;
}
// Check if we over the target length or we are close to it, if so we load the next playlist.
if next_start >= target_length
|| is_close(total_delta, 0.0, 2.0)
|| is_close(total_delta, target_length, 2.0)
@ -182,8 +187,8 @@ impl CurrentProgram {
self.json_path = json.current_file.clone();
self.json_mod = json.modified;
self.json_date = json.date;
*self.nodes.lock().unwrap() = json.program;
self.index.store(0, Ordering::SeqCst);
*self.player_control.current_list.lock().unwrap() = json.program;
self.player_control.current_index.store(0, Ordering::SeqCst);
if json.current_file.is_none() {
self.playout_stat.list_init.store(true, Ordering::SeqCst);
@ -193,8 +198,8 @@ impl CurrentProgram {
// Check if last and/or next clip is a advertisement.
fn last_next_ad(&mut self) {
let index = self.index.load(Ordering::SeqCst);
let current_list = self.nodes.lock().unwrap();
let index = self.player_control.current_index.load(Ordering::SeqCst);
let current_list = self.player_control.current_list.lock().unwrap();
if index + 1 < current_list.len() && &current_list[index + 1].category == "advertisement" {
self.current_node.next_ad = Some(true);
@ -233,10 +238,17 @@ impl CurrentProgram {
time_sec += *shift;
}
for (i, item) in self.nodes.lock().unwrap().iter_mut().enumerate() {
for (i, item) in self
.player_control
.current_list
.lock()
.unwrap()
.iter_mut()
.enumerate()
{
if item.begin.unwrap() + item.out - item.seek > time_sec {
self.playout_stat.list_init.store(false, Ordering::SeqCst);
self.index.store(i, Ordering::SeqCst);
self.player_control.current_index.store(i, Ordering::SeqCst);
break;
}
@ -249,8 +261,11 @@ impl CurrentProgram {
if !self.playout_stat.list_init.load(Ordering::SeqCst) {
let time_sec = self.get_current_time();
let index = self.index.fetch_add(1, Ordering::SeqCst);
let nodes = self.nodes.lock().unwrap();
let index = self
.player_control
.current_index
.fetch_add(1, Ordering::SeqCst);
let nodes = self.player_control.current_list.lock().unwrap();
let last_index = nodes.len() - 1;
// de-instance node to preserve original values in list
@ -261,6 +276,7 @@ impl CurrentProgram {
&self.config,
node_clone,
&self.playout_stat.chain,
&self.player_control,
last_index,
);
}
@ -284,10 +300,11 @@ impl Iterator for CurrentProgram {
// On init load, playlist could be not long enough,
// so we check if we can take the next playlist already,
// or we fill the gap with a dummy.
let last_index = self.nodes.lock().unwrap().len() - 1;
self.current_node = self.nodes.lock().unwrap()[last_index].clone();
let new_node = self.nodes.lock().unwrap()[last_index].clone();
let new_length = new_node.begin.unwrap() + new_node.duration;
let last_index = self.player_control.current_list.lock().unwrap().len() - 1;
self.current_node =
self.player_control.current_list.lock().unwrap()[last_index].clone();
let new_node = self.player_control.current_list.lock().unwrap()[last_index].clone();
let new_length = new_node.begin.unwrap_or_default() + new_node.duration;
trace!("Init playlist after playlist end");
self.check_for_next_playlist();
@ -301,10 +318,35 @@ impl Iterator for CurrentProgram {
// fill missing length from playlist
let mut current_time = get_sec();
let (_, total_delta) = get_delta(&self.config, &current_time);
let mut duration = DUMMY_LEN;
let mut out = total_delta.abs();
let mut duration = out + 0.001;
if DUMMY_LEN > total_delta {
trace!("Total delta on list init: {total_delta}");
let filler_index = self.player_control.filler_index.load(Ordering::SeqCst);
let mut filler =
self.player_control.filler_list.lock().unwrap()[filler_index].clone();
filler.add_probe();
// If there is no filler, the duration of the dummy clip should not be too long.
// This would take away the ability to restart the playlist when the playout registers a change.
if filler.duration > 0.0 {
if duration > filler.duration {
out = filler.duration;
}
duration = filler.duration;
} else if DUMMY_LEN > total_delta {
duration = total_delta;
out = total_delta;
} else {
duration = DUMMY_LEN;
out = DUMMY_LEN;
}
if self.json_path.is_some() {
// When playlist is missing, we always need to init the playlist the next iteration.
self.playout_stat.list_init.store(false, Ordering::SeqCst);
}
@ -312,19 +354,26 @@ impl Iterator for CurrentProgram {
current_time += self.config.playlist.length_sec.unwrap() + 1.0;
}
let mut nodes = self.nodes.lock().unwrap();
let mut nodes = self.player_control.current_list.lock().unwrap();
let index = nodes.len();
let mut media = Media::new(index, "", false);
media.begin = Some(current_time);
media.duration = duration;
media.out = duration;
media.out = out;
self.current_node =
gen_source(&self.config, media, &self.playout_stat.chain, last_index);
self.current_node = gen_source(
&self.config,
media,
&self.playout_stat.chain,
&self.player_control,
last_index,
);
nodes.push(self.current_node.clone());
self.index.store(nodes.len(), Ordering::SeqCst);
self.player_control
.current_index
.store(nodes.len(), Ordering::SeqCst);
}
}
@ -333,11 +382,13 @@ impl Iterator for CurrentProgram {
return Some(self.current_node.clone());
}
if self.index.load(Ordering::SeqCst) < self.nodes.lock().unwrap().len() {
if self.player_control.current_index.load(Ordering::SeqCst)
< self.player_control.current_list.lock().unwrap().len()
{
self.check_for_next_playlist();
let mut is_last = false;
let index = self.index.load(Ordering::SeqCst);
let nodes = self.nodes.lock().unwrap();
let index = self.player_control.current_index.load(Ordering::SeqCst);
let nodes = self.player_control.current_list.lock().unwrap();
let last_index = nodes.len() - 1;
if index == last_index {
@ -349,12 +400,15 @@ impl Iterator for CurrentProgram {
&self.config,
is_last,
&self.playout_stat,
&self.player_control,
last_index,
);
drop(nodes);
self.last_next_ad();
self.index.fetch_add(1, Ordering::SeqCst);
self.player_control
.current_index
.fetch_add(1, Ordering::SeqCst);
Some(self.current_node.clone())
} else {
@ -368,49 +422,76 @@ impl Iterator for CurrentProgram {
&& last_playlist == self.json_path
&& total_delta.abs() > 1.0
{
// Test if playlist is to early finish,
trace!("Total delta on list end: {total_delta}");
// Playlist is to early finish,
// and if we have to fill it with a placeholder.
let index = self.index.load(Ordering::SeqCst);
let index = self.player_control.current_index.load(Ordering::SeqCst);
self.current_node = Media::new(index, "", false);
self.current_node.begin = Some(get_sec());
let mut duration = total_delta.abs();
let mut out = total_delta.abs();
let mut duration = out + 0.001;
if duration > DUMMY_LEN {
let filler_index = self.player_control.filler_index.load(Ordering::SeqCst);
let mut filler =
self.player_control.filler_list.lock().unwrap()[filler_index].clone();
filler.add_probe();
if filler.duration > 0.0 {
if duration > filler.duration {
out = filler.duration;
}
duration = filler.duration;
} else if DUMMY_LEN > total_delta {
duration = total_delta;
out = total_delta;
} else {
duration = DUMMY_LEN;
out = DUMMY_LEN;
}
self.current_node.duration = duration;
self.current_node.out = duration;
self.current_node.out = out;
self.current_node = gen_source(
&self.config,
self.current_node.clone(),
&self.playout_stat.chain,
&self.player_control,
0,
);
self.nodes.lock().unwrap().push(self.current_node.clone());
self.player_control
.current_list
.lock()
.unwrap()
.push(self.current_node.clone());
self.last_next_ad();
self.current_node.last_ad = last_ad;
self.current_node
.add_filter(&self.config, &self.playout_stat.chain);
self.index.fetch_add(1, Ordering::SeqCst);
self.player_control
.current_index
.fetch_add(1, Ordering::SeqCst);
return Some(self.current_node.clone());
}
// Get first clip from next playlist.
self.index.store(0, Ordering::SeqCst);
let last_index = self.nodes.lock().unwrap().len() - 1;
self.player_control.current_index.store(0, Ordering::SeqCst);
self.current_node = gen_source(
&self.config,
self.nodes.lock().unwrap()[0].clone(),
self.player_control.current_list.lock().unwrap()[0].clone(),
&self.playout_stat.chain,
last_index,
&self.player_control,
0,
);
self.last_next_ad();
self.current_node.last_ad = last_ad;
self.index.store(1, Ordering::SeqCst);
self.player_control.current_index.store(1, Ordering::SeqCst);
Some(self.current_node.clone())
}
@ -426,6 +507,7 @@ fn timed_source(
config: &PlayoutConfig,
last: bool,
playout_stat: &PlayoutStatus,
player_control: &PlayerControl,
last_index: usize,
) -> Media {
let (delta, total_delta) = get_delta(config, &node.begin.unwrap());
@ -463,11 +545,24 @@ fn timed_source(
{
// when we are in the 24 hour range, get the clip
new_node.process = Some(true);
new_node = gen_source(config, node, &playout_stat.chain, last_index);
new_node = gen_source(
config,
node,
&playout_stat.chain,
player_control,
last_index,
);
} else if total_delta <= 0.0 {
info!("Begin is over play time, skip: {}", node.source);
} else if total_delta < node.duration - node.seek || last {
new_node = handle_list_end(config, node, total_delta, &playout_stat.chain, last_index);
new_node = handle_list_end(
config,
node,
total_delta,
&playout_stat.chain,
player_control,
last_index,
);
}
new_node
@ -478,10 +573,13 @@ pub fn gen_source(
config: &PlayoutConfig,
mut node: Media,
filter_chain: &Option<Arc<Mutex<Vec<String>>>>,
player_control: &PlayerControl,
last_index: usize,
) -> Media {
let duration = node.out - node.seek;
trace!("Clip out: {duration}, duration: {}", node.duration);
if valid_source(&node.source) {
node.add_probe();
@ -507,39 +605,78 @@ pub fn gen_source(
error!("Source not found: <b><magenta>\"{}\"</></b>", node.source);
}
warn!("Generate filler with <yellow>{duration:.2}</> seconds length!");
let filler_source = Path::new(&config.storage.filler);
let probe = MediaProbe::new(&config.storage.filler_clip);
if filler_source.is_dir() && !player_control.filler_list.lock().unwrap().is_empty() {
let filler_index = player_control.filler_index.fetch_add(1, Ordering::SeqCst);
let mut filler_media = player_control.filler_list.lock().unwrap()[filler_index].clone();
if config
.storage
.filler_clip
.rsplit_once('.')
.map(|(_, e)| e.to_lowercase())
.filter(|c| IMAGE_FORMAT.contains(&c.as_str()))
.is_some()
{
node.source = config.storage.filler_clip.clone();
node.cmd = Some(loop_image(&node));
node.probe = Some(probe);
} else if let Some(length) = probe
.clone()
.format
.and_then(|f| f.duration)
.and_then(|d| d.parse::<f64>().ok())
{
// create placeholder from config filler.
node.source = config.storage.filler_clip.clone();
node.duration = length;
node.out = duration;
if filler_index == player_control.filler_list.lock().unwrap().len() - 1 {
player_control.filler_index.store(0, Ordering::SeqCst)
}
if filler_media.probe.is_none() {
filler_media.add_probe();
}
if node.duration > duration && filler_media.duration > duration {
filler_media.out = duration;
}
node.source = filler_media.source;
node.duration = filler_media.duration;
node.out = filler_media.out;
node.cmd = Some(loop_filler(&node));
node.probe = Some(probe);
node.probe = filler_media.probe;
} else if filler_source.is_file() {
let probe = MediaProbe::new(&config.storage.filler);
if config
.storage
.filler
.rsplit_once('.')
.map(|(_, e)| e.to_lowercase())
.filter(|c| IMAGE_FORMAT.contains(&c.as_str()))
.is_some()
{
node.source = config.storage.filler.clone();
node.cmd = Some(loop_image(&node));
node.probe = Some(probe);
} else if let Some(filler_duration) = probe
.clone()
.format
.and_then(|f| f.duration)
.and_then(|d| d.parse::<f64>().ok())
{
// Create placeholder from config filler.
node.source = config.storage.filler.clone();
node.out = if node.duration > duration && filler_duration > duration {
duration
} else {
filler_duration
};
node.duration = filler_duration;
node.cmd = Some(loop_filler(&node));
node.probe = Some(probe);
} else {
// Create colored placeholder.
let (source, cmd) = gen_dummy(config, duration);
node.source = source;
node.cmd = Some(cmd);
}
} else {
// create colored placeholder.
// Create colored placeholder.
let (source, cmd) = gen_dummy(config, duration);
node.source = source;
node.cmd = Some(cmd);
}
warn!(
"Generate filler with <yellow>{:.2}</> seconds length!",
node.out
);
}
node.add_filter(config, filter_chain);
@ -562,6 +699,7 @@ fn handle_list_init(
config: &PlayoutConfig,
mut node: Media,
filter_chain: &Option<Arc<Mutex<Vec<String>>>>,
player_control: &PlayerControl,
last_index: usize,
) -> Media {
debug!("Playlist init");
@ -574,7 +712,7 @@ fn handle_list_init(
node.out = out;
gen_source(config, node, filter_chain, last_index)
gen_source(config, node, filter_chain, player_control, last_index)
}
/// when we come to last clip in playlist,
@ -585,6 +723,7 @@ fn handle_list_end(
mut node: Media,
total_delta: f64,
filter_chain: &Option<Arc<Mutex<Vec<String>>>>,
player_control: &PlayerControl,
last_index: usize,
) -> Media {
debug!("Playlist end");
@ -613,5 +752,5 @@ fn handle_list_end(
node.process = Some(true);
gen_source(config, node, filter_chain, last_index)
gen_source(config, node, filter_chain, player_control, last_index)
}

View File

@ -19,9 +19,9 @@ use ffplayout::{
};
use ffplayout_lib::utils::{
generate_playlist, get_date, import::import_file, init_logging, is_remote, send_mail,
test_tcp_port, validate_ffmpeg, validate_playlist, JsonPlaylist, OutputMode::*, PlayerControl,
PlayoutStatus, ProcessControl,
folder::fill_filler_list, generate_playlist, get_date, import::import_file, init_logging,
is_remote, send_mail, test_tcp_port, validate_ffmpeg, validate_playlist, JsonPlaylist,
OutputMode::*, PlayerControl, PlayoutStatus, ProcessControl,
};
#[cfg(debug_assertions)]
@ -99,7 +99,8 @@ fn main() {
let play_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new();
let proc_control = ProcessControl::new();
let play_ctl = play_control.clone();
let play_ctl1 = play_control.clone();
let play_ctl2 = play_control.clone();
let play_stat = playout_stat.clone();
let proc_ctl1 = proc_control.clone();
let proc_ctl2 = proc_control.clone();
@ -122,7 +123,8 @@ fn main() {
exit(1);
};
let config_clone = config.clone();
let config_clone1 = config.clone();
let config_clone2 = config.clone();
if ![2, 4, 6, 8].contains(&config.processing.audio_channels) {
error!(
@ -200,7 +202,7 @@ fn main() {
exit(1)
}
thread::spawn(move || run_server(config_clone, play_ctl, play_stat, proc_ctl2));
thread::spawn(move || run_server(config_clone1, play_ctl1, play_stat, proc_ctl2));
}
status_file(&config.general.stat_file, &playout_stat);
@ -210,11 +212,14 @@ fn main() {
config.general.config_path
);
// Fill filler list, can also be a single file.
thread::spawn(move || fill_filler_list(config_clone2, play_ctl2));
match config.out.mode {
// write files/playlist to HLS m3u8 playlist
HLS => write_hls(&config, play_control, playout_stat, proc_control),
// play on desktop or stream to a remote target
_ => player(&config, play_control, playout_stat, proc_control),
_ => player(&config, &play_control, playout_stat, proc_control),
}
info!("Playout done...");

View File

@ -2,9 +2,7 @@ use std::process::{self, Command, Stdio};
use simplelog::*;
use ffplayout_lib::filter::v_drawtext;
use ffplayout_lib::utils::PlayoutConfig;
use ffplayout_lib::vec_strings;
use ffplayout_lib::{filter::v_drawtext, utils::PlayoutConfig, vec_strings};
/// Desktop Output
///
@ -26,6 +24,7 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child {
if let Some(mut cmd) = config.out.output_cmd.clone() {
if !cmd.iter().any(|i| {
[
"-c",
"-c:v",
"-c:v:0",
"-b:v",
@ -41,7 +40,7 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child {
}) {
enc_cmd.append(&mut cmd);
} else {
warn!("Given output parameters are skipped, they are not supported by ffplay!");
warn!("ffplay does not support given output parameters, they are skipped!");
}
}

View File

@ -31,7 +31,7 @@ use crate::input::source_generator;
use crate::utils::{log_line, prepare_output_cmd, valid_stream};
use ffplayout_lib::{
utils::{
controller::ProcessUnit::*, sec_to_time, stderr_reader, test_tcp_port, Media,
controller::ProcessUnit::*, get_delta, sec_to_time, stderr_reader, test_tcp_port, Media,
PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl,
},
vec_strings,
@ -137,7 +137,7 @@ fn ingest_to_hls_server(
/// Write with single ffmpeg instance directly to a HLS playlist.
pub fn write_hls(
config: &PlayoutConfig,
play_control: PlayerControl,
player_control: PlayerControl,
playout_stat: PlayoutStatus,
proc_control: ProcessControl,
) {
@ -148,8 +148,7 @@ pub fn write_hls(
let get_source = source_generator(
config.clone(),
play_control.current_list.clone(),
play_control.index.clone(),
&player_control,
playout_stat,
proc_control.is_terminated.clone(),
);
@ -160,7 +159,7 @@ pub fn write_hls(
}
for node in get_source {
*play_control.current_media.lock().unwrap() = Some(node.clone());
*player_control.current_media.lock().unwrap() = Some(node.clone());
let mut cmd = match node.cmd {
Some(cmd) => cmd,
@ -178,6 +177,25 @@ pub fn write_hls(
);
let mut enc_prefix = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format];
let mut read_rate = 1.0;
if let Some(begin) = &node.begin {
let (delta, _) = get_delta(config, begin);
let duration = node.out - node.seek;
let speed = duration / (duration + delta);
if node.seek == 0.0
&& speed > 0.0
&& speed < 1.3
&& delta < config.general.stop_threshold
{
read_rate = speed;
}
}
enc_prefix.append(&mut vec_strings!["-readrate", read_rate]);
enc_prefix.append(&mut cmd);
let enc_cmd = prepare_output_cmd(config, enc_prefix, &node.filter);

View File

@ -1,5 +1,6 @@
use std::{
io::{prelude::*, BufReader, BufWriter, Read},
path::Path,
process::{Command, Stdio},
sync::atomic::Ordering,
thread::{self, sleep},
@ -17,6 +18,8 @@ mod stream;
pub use hls::write_hls;
use crate::input::{ingest_server, source_generator};
use crate::utils::task_runner;
use ffplayout_lib::utils::{
sec_to_time, stderr_reader, OutputMode::*, PlayerControl, PlayoutConfig, PlayoutStatus,
ProcessControl, ProcessUnit::*,
@ -34,7 +37,7 @@ use ffplayout_lib::vec_strings;
/// When ingest stops, it switch back to playlist/folder mode.
pub fn player(
config: &PlayoutConfig,
play_control: PlayerControl,
play_control: &PlayerControl,
playout_stat: PlayoutStatus,
proc_control: ProcessControl,
) {
@ -47,8 +50,7 @@ pub fn player(
// get source iterator
let get_source = source_generator(
config.clone(),
play_control.current_list.clone(),
play_control.index.clone(),
play_control,
playout_stat,
proc_control.is_terminated.clone(),
);
@ -83,11 +85,6 @@ pub fn player(
'source_iter: for node in get_source {
*play_control.current_media.lock().unwrap() = Some(node.clone());
let mut cmd = match node.cmd {
Some(cmd) => cmd,
None => break,
};
if !node.process.unwrap() {
continue;
}
@ -99,6 +96,28 @@ pub fn player(
node.audio
);
if config.task.enable {
let task_config = config.clone();
let task_node = node.clone();
let server_running = proc_control.server_is_running.load(Ordering::SeqCst);
if Path::new(&config.task.path).is_file() {
thread::spawn(move || task_runner::run(task_config, task_node, server_running));
} else {
error!(
"<bright-blue>{}</> executable not exists!",
config.task.path
);
}
}
trace!("Decoder CMD: {:?}", node.cmd);
let mut cmd = match node.cmd {
Some(cmd) => cmd,
None => break,
};
let mut dec_cmd = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format];
dec_cmd.append(&mut cmd);
@ -206,6 +225,8 @@ pub fn player(
};
}
trace!("Out of source loop");
sleep(Duration::from_secs(1));
proc_control.stop_all();

View File

@ -1,21 +1,26 @@
use std::{fmt, sync::atomic::Ordering};
use regex::Regex;
extern crate serde;
extern crate serde_json;
extern crate tiny_http;
use futures::executor::block_on;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use serde::{
de::{self, Visitor},
Deserialize, Serialize,
};
use serde_json::{json, Map};
use simplelog::*;
use std::collections::HashMap;
use std::io::{Cursor, Error as IoError};
use tiny_http::{Header, Method, Request, Response, Server};
use crate::rpc::zmq_send;
use crate::utils::{get_data_map, get_media_map};
use ffplayout_lib::utils::{
get_delta, get_sec, sec_to_time, write_status, Ingest, Media, OutputMode::*, PlayerControl,
PlayoutConfig, PlayoutStatus, ProcessControl,
get_delta, write_status, Ingest, OutputMode::*, PlayerControl, PlayoutConfig, PlayoutStatus,
ProcessControl,
};
#[derive(Default, Deserialize, Clone, Debug)]
@ -39,24 +44,40 @@ struct TextFilter {
boxborderw: Option<String>,
}
fn deserialize_number_or_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
/// Deserialize number or string
pub fn deserialize_number_or_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value: Value = Deserialize::deserialize(deserializer)?;
match value {
Value::Number(num) => {
if let Some(number) = num.as_i64() {
Ok(Some(number.to_string()))
} else if let Some(number) = num.as_f64() {
Ok(Some(number.to_string()))
} else {
Err(serde::de::Error::custom("Invalid number format"))
}
struct StringOrNumberVisitor;
impl<'de> Visitor<'de> for StringOrNumberVisitor {
type Value = Option<String>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or a number")
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
let re = Regex::new(r"0,([0-9]+)").unwrap();
let clean_string = re.replace_all(value, "0.$1").to_string();
Ok(Some(clean_string))
}
fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
Ok(Some(value.to_string()))
}
fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
Ok(Some(value.to_string()))
}
fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
Ok(Some(value.to_string()))
}
Value::String(string) => Ok(Some(string)),
_ => Err(serde::de::Error::custom("Invalid value type")),
}
deserializer.deserialize_any(StringOrNumberVisitor)
}
impl fmt::Display for TextFilter {
@ -129,45 +150,6 @@ fn filter_from_json(raw_text: serde_json::Value) -> String {
filter.to_string()
}
/// map media struct to json object
fn get_media_map(media: Media) -> Value {
json!({
"seek": media.seek,
"out": media.out,
"duration": media.duration,
"category": media.category,
"source": media.source,
})
}
/// prepare json object for response
fn get_data_map(
config: &PlayoutConfig,
media: Media,
server_is_running: bool,
) -> Map<String, Value> {
let mut data_map = Map::new();
let begin = media.begin.unwrap_or(0.0);
data_map.insert("play_mode".to_string(), json!(config.processing.mode));
data_map.insert("ingest_runs".to_string(), json!(server_is_running));
data_map.insert("index".to_string(), json!(media.index));
data_map.insert("start_sec".to_string(), json!(begin));
if begin > 0.0 {
let played_time = get_sec() - begin;
let remaining_time = media.out - played_time;
data_map.insert("start_time".to_string(), json!(sec_to_time(begin)));
data_map.insert("played_sec".to_string(), json!(played_time));
data_map.insert("remaining_sec".to_string(), json!(remaining_time));
}
data_map.insert("current_media".to_string(), get_media_map(media));
data_map
}
#[derive(Debug, Serialize, Deserialize)]
struct ResponseData {
message: String,
@ -213,7 +195,7 @@ fn control_back(
let current_date = playout_stat.current_date.lock().unwrap().clone();
let current_list = play_control.current_list.lock().unwrap();
let mut date = playout_stat.date.lock().unwrap();
let index = play_control.index.load(Ordering::SeqCst);
let index = play_control.current_index.load(Ordering::SeqCst);
let mut time_shift = playout_stat.time_shift.lock().unwrap();
if index > 1 && current_list.len() > 1 {
@ -229,7 +211,7 @@ fn control_back(
info!("Move to last clip");
let mut data_map = Map::new();
let mut media = current_list[index - 2].clone();
play_control.index.fetch_sub(2, Ordering::SeqCst);
play_control.current_index.fetch_sub(2, Ordering::SeqCst);
media.add_probe();
let (delta, _) = get_delta(config, &media.begin.unwrap_or(0.0));
@ -259,7 +241,7 @@ fn control_next(
let current_date = playout_stat.current_date.lock().unwrap().clone();
let current_list = play_control.current_list.lock().unwrap();
let mut date = playout_stat.date.lock().unwrap();
let index = play_control.index.load(Ordering::SeqCst);
let index = play_control.current_index.load(Ordering::SeqCst);
let mut time_shift = playout_stat.time_shift.lock().unwrap();
if index < current_list.len() {
@ -408,7 +390,7 @@ fn media_current(
/// media info: get infos about next clip
fn media_next(config: &PlayoutConfig, play_control: &PlayerControl) -> Response<Cursor<Vec<u8>>> {
let index = play_control.index.load(Ordering::SeqCst);
let index = play_control.current_index.load(Ordering::SeqCst);
let current_list = play_control.current_list.lock().unwrap();
if index < current_list.len() {
@ -424,7 +406,7 @@ fn media_next(config: &PlayoutConfig, play_control: &PlayerControl) -> Response<
/// media info: get infos about last clip
fn media_last(config: &PlayoutConfig, play_control: &PlayerControl) -> Response<Cursor<Vec<u8>>> {
let index = play_control.index.load(Ordering::SeqCst);
let index = play_control.current_index.load(Ordering::SeqCst);
let current_list = play_control.current_list.lock().unwrap();
if index > 1 && index - 2 < current_list.len() {

View File

@ -66,6 +66,9 @@ pub struct Args {
)]
pub length: Option<String>,
#[clap(long, help = "Override logging level")]
pub level: Option<String>,
#[clap(short, long, help = "Loop playlist infinitely")]
pub infinit: bool,

View File

@ -4,14 +4,19 @@ use std::{
};
use regex::Regex;
use serde_json::{json, Map, Value};
use simplelog::*;
pub mod arg_parse;
pub mod task_runner;
pub use arg_parse::Args;
use ffplayout_lib::{
filter::Filters,
utils::{time_to_sec, OutputMode::*, PlayoutConfig, ProcessMode::*},
utils::{
get_sec, parse_log_level_filter, sec_to_time, time_to_sec, Media, OutputMode::*,
PlayoutConfig, ProcessMode::*,
},
vec_strings,
};
@ -82,6 +87,12 @@ pub fn get_config(args: Args) -> PlayoutConfig {
}
}
if let Some(level) = args.level {
if let Ok(filter) = parse_log_level_filter(&level) {
config.logging.level = filter;
}
}
if args.infinit {
config.playlist.infinit = args.infinit;
}
@ -207,3 +218,42 @@ pub fn prepare_output_cmd(
cmd
}
/// map media struct to json object
pub fn get_media_map(media: Media) -> Value {
json!({
"seek": media.seek,
"out": media.out,
"duration": media.duration,
"category": media.category,
"source": media.source,
})
}
/// prepare json object for response
pub fn get_data_map(
config: &PlayoutConfig,
media: Media,
server_is_running: bool,
) -> Map<String, Value> {
let mut data_map = Map::new();
let begin = media.begin.unwrap_or(0.0);
data_map.insert("play_mode".to_string(), json!(config.processing.mode));
data_map.insert("ingest_runs".to_string(), json!(server_is_running));
data_map.insert("index".to_string(), json!(media.index));
data_map.insert("start_sec".to_string(), json!(begin));
if begin > 0.0 {
let played_time = get_sec() - begin;
let remaining_time = media.out - played_time;
data_map.insert("start_time".to_string(), json!(sec_to_time(begin)));
data_map.insert("played_sec".to_string(), json!(played_time));
data_map.insert("remaining_sec".to_string(), json!(remaining_time));
}
data_map.insert("current_media".to_string(), get_media_map(media));
data_map
}

View File

@ -0,0 +1,24 @@
use std::process::Command;
use simplelog::*;
use crate::utils::get_data_map;
use ffplayout_lib::utils::{config::PlayoutConfig, Media};
pub fn run(config: PlayoutConfig, node: Media, server_running: bool) {
let obj = serde_json::to_string(&get_data_map(&config, node, server_running)).unwrap();
trace!("Run task: {obj}");
match Command::new(config.task.path).arg(obj).spawn() {
Ok(mut c) => {
let status = c.wait().expect("Error in waiting for the task process!");
if !status.success() {
error!("Process stops with error.");
}
}
Err(e) => {
error!("Couldn't spawn task runner: {e}")
}
}
}

@ -1 +1 @@
Subproject commit 3333aa8992e6d571bccf6da4beefc3fbb86c56ff
Subproject commit 2f3234221a0aef8e70d9e2b5e9bbfb1fe51921fc

View File

@ -11,8 +11,7 @@ mod custom;
pub mod v_drawtext;
use crate::utils::{
controller::ProcessUnit::*, fps_calc, get_delta, is_close, Media, MediaProbe, OutputMode::*,
PlayoutConfig,
controller::ProcessUnit::*, fps_calc, is_close, Media, MediaProbe, OutputMode::*, PlayoutConfig,
};
use super::vec_strings;
@ -402,38 +401,6 @@ fn aspect_calc(aspect_string: &Option<String>, config: &PlayoutConfig) -> f64 {
source_aspect
}
/// This realtime filter is important for HLS output to stay in sync.
fn realtime(
node: &mut Media,
chain: &mut Filters,
config: &PlayoutConfig,
filter_type: FilterType,
) {
if config.general.generate.is_none() && config.out.mode == HLS {
let prefix = match filter_type {
Audio => "a",
Video => "",
};
let mut speed_filter = format!("{prefix}realtime=speed=1");
if let Some(begin) = &node.begin {
let (delta, _) = get_delta(config, begin);
if delta < 0.0 && node.seek == 0.0 {
let duration = node.out - node.seek;
let speed = duration / (duration + delta);
if speed > 0.0 && speed < 1.1 && delta < config.general.stop_threshold {
speed_filter = format!("{prefix}realtime=speed={speed}");
}
}
}
chain.add_filter(&speed_filter, 0, filter_type);
}
}
pub fn split_filter(chain: &mut Filters, count: usize, nr: i32, filter_type: FilterType) {
if count > 1 {
let out_link = match filter_type {
@ -518,7 +485,7 @@ pub fn filter_chains(
return filters;
}
if !config.processing.audio_only {
if !config.processing.audio_only && !config.processing.copy_video {
if let Some(probe) = node.probe.as_ref() {
if Path::new(&node.audio).is_file() {
filters.audio_position = 1;
@ -549,7 +516,6 @@ pub fn filter_chains(
add_text(node, &mut filters, config, filter_chain);
fade(node, &mut filters, 0, Video);
overlay(node, &mut filters, config);
realtime(node, &mut filters, config, Video);
}
let (proc_vf, proc_af) = if node.unit == Ingest {
@ -560,39 +526,54 @@ pub fn filter_chains(
let (list_vf, list_af) = custom::filter_node(&node.custom_filter);
if config.processing.audio_only {
realtime(node, &mut filters, config, Audio);
} else {
if !config.processing.copy_video {
custom(&proc_vf, &mut filters, 0, Video);
custom(&list_vf, &mut filters, 0, Video);
}
for i in 0..config.processing.audio_tracks {
if node
.probe
.as_ref()
.and_then(|p| p.audio_streams.get(i as usize))
.is_some()
|| Path::new(&node.audio).is_file()
{
extend_audio(node, &mut filters, i);
} else if node.unit == Decoder {
warn!(
"Missing audio track (id {i}) from <b><magenta>{}</></b>",
node.source
);
add_audio(node, &mut filters, i);
let mut audio_indexes = vec![];
if config.processing.audio_track_index == -1 {
for i in 0..config.processing.audio_tracks {
audio_indexes.push(i)
}
} else {
audio_indexes.push(config.processing.audio_track_index)
}
// add at least anull filter, for correct filter construction,
// is important for split filter in HLS mode
filters.add_filter("anull", i, Audio);
if !config.processing.copy_audio {
for i in audio_indexes {
if node
.probe
.as_ref()
.and_then(|p| p.audio_streams.get(i as usize))
.is_some()
|| Path::new(&node.audio).is_file()
{
extend_audio(node, &mut filters, i);
} else if node.unit == Decoder {
if !node.source.contains("color=c=") {
warn!(
"Missing audio track (id {i}) from <b><magenta>{}</></b>",
node.source
);
}
fade(node, &mut filters, i, Audio);
audio_volume(&mut filters, config, i);
add_audio(node, &mut filters, i);
}
custom(&proc_af, &mut filters, i, Audio);
custom(&list_af, &mut filters, i, Audio);
// add at least anull filter, for correct filter construction,
// is important for split filter in HLS mode
filters.add_filter("anull", i, Audio);
fade(node, &mut filters, i, Audio);
audio_volume(&mut filters, config, i);
custom(&proc_af, &mut filters, i, Audio);
custom(&list_af, &mut filters, i, Audio);
}
} else if config.processing.audio_track_index > -1 {
error!("Setting 'audio_track_index' other than '-1' is not allowed in audio copy mode!")
}
if config.out.mode == HLS {

View File

@ -25,7 +25,6 @@ pub fn filter_node(
_ => config.text.zmq_stream_socket.clone(),
};
// TODO: in Rust 1.66 use let_chains instead
if config.text.text_from_filename && node.is_some() {
let source = node.unwrap_or(&Media::new(0, "", false)).source.clone();
let text = match Regex::new(&config.text.regex)

View File

@ -138,6 +138,8 @@ pub struct PlayoutConfig {
pub playlist: Playlist,
pub storage: Storage,
pub text: Text,
#[serde(default)]
pub task: Task,
pub out: Out,
}
@ -211,6 +213,12 @@ pub struct Processing {
pub mode: ProcessMode,
#[serde(default)]
pub audio_only: bool,
#[serde(default = "default_track_index")]
pub audio_track_index: i32,
#[serde(default)]
pub copy_audio: bool,
#[serde(default)]
pub copy_video: bool,
pub width: i64,
pub height: i64,
pub aspect: f64,
@ -267,7 +275,8 @@ pub struct Storage {
pub path: String,
#[serde(skip_serializing, skip_deserializing)]
pub paths: Vec<String>,
pub filler_clip: String,
#[serde(alias = "filler_clip")]
pub filler: String,
pub extensions: Vec<String>,
pub shuffle: bool,
}
@ -292,6 +301,12 @@ pub struct Text {
pub regex: String,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct Task {
pub enable: bool,
pub path: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Out {
pub help_text: String,
@ -306,6 +321,10 @@ pub struct Out {
pub output_cmd: Option<Vec<String>>,
}
fn default_track_index() -> i32 {
-1
}
fn default_tracks() -> i32 {
1
}
@ -397,6 +416,8 @@ impl PlayoutConfig {
if config.processing.audio_only {
process_cmd.append(&mut vec_strings!["-vn"]);
} else if config.processing.copy_video {
process_cmd.append(&mut vec_strings!["-c:v", "copy"]);
} else {
process_cmd.append(&mut vec_strings![
"-pix_fmt",
@ -418,19 +439,17 @@ impl PlayoutConfig {
]);
}
process_cmd.append(&mut pre_audio_codec(
&config.processing.custom_filter,
&config.ingest.custom_filter,
));
process_cmd.append(&mut vec_strings![
"-ar",
"48000",
"-ac",
config.processing.audio_channels,
"-f",
"mpegts",
"-"
]);
if config.processing.copy_audio {
process_cmd.append(&mut vec_strings!["-c:a", "copy"]);
} else {
process_cmd.append(&mut pre_audio_codec(
&config.processing.custom_filter,
&config.ingest.custom_filter,
config.processing.audio_channels,
));
}
process_cmd.append(&mut vec_strings!["-f", "mpegts", "-"]);
config.processing.cmd = Some(process_cmd);
@ -489,11 +508,31 @@ impl Default for PlayoutConfig {
/// When custom_filter contains loudnorm filter use a different audio encoder,
/// s302m has higher quality, but is experimental
/// and works not well together with the loudnorm filter.
fn pre_audio_codec(proc_filter: &str, ingest_filter: &str) -> Vec<String> {
let mut codec = vec_strings!["-c:a", "s302m", "-strict", "-2", "-sample_fmt", "s16"];
fn pre_audio_codec(proc_filter: &str, ingest_filter: &str, channel_count: u8) -> Vec<String> {
let mut codec = vec_strings![
"-c:a",
"s302m",
"-strict",
"-2",
"-sample_fmt",
"s16",
"-ar",
"48000",
"-ac",
channel_count
];
if proc_filter.contains("loudnorm") || ingest_filter.contains("loudnorm") {
codec = vec_strings!["-c:a", "mp2", "-b:a", "384k"];
codec = vec_strings![
"-c:a",
"mp2",
"-b:a",
"384k",
"-ar",
"48000",
"-ac",
channel_count
];
}
codec

View File

@ -163,11 +163,13 @@ impl ProcessControl {
// }
/// Global player control, to get infos about current clip etc.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct PlayerControl {
pub current_media: Arc<Mutex<Option<Media>>>,
pub current_list: Arc<Mutex<Vec<Media>>>,
pub index: Arc<AtomicUsize>,
pub filler_list: Arc<Mutex<Vec<Media>>>,
pub current_index: Arc<AtomicUsize>,
pub filler_index: Arc<AtomicUsize>,
}
impl PlayerControl {
@ -175,7 +177,9 @@ impl PlayerControl {
Self {
current_media: Arc::new(Mutex::new(None)),
current_list: Arc::new(Mutex::new(vec![Media::new(0, "", false)])),
index: Arc::new(AtomicUsize::new(0)),
filler_list: Arc::new(Mutex::new(vec![Media::new(0, "", false)])),
current_index: Arc::new(AtomicUsize::new(0)),
filler_index: Arc::new(AtomicUsize::new(0)),
}
}
}

View File

@ -1,7 +1,7 @@
use std::{
path::Path,
sync::{
atomic::{AtomicUsize, Ordering},
atomic::Ordering,
{Arc, Mutex},
},
};
@ -10,7 +10,9 @@ use rand::{seq::SliceRandom, thread_rng};
use simplelog::*;
use walkdir::WalkDir;
use crate::utils::{get_sec, include_file, Media, PlayoutConfig};
use crate::utils::{
controller::PlayerControl, get_sec, include_file_extension, Media, PlayoutConfig,
};
/// Folder Sources
///
@ -19,17 +21,15 @@ use crate::utils::{get_sec, include_file, Media, PlayoutConfig};
pub struct FolderSource {
config: PlayoutConfig,
filter_chain: Option<Arc<Mutex<Vec<String>>>>,
pub nodes: Arc<Mutex<Vec<Media>>>,
pub player_control: PlayerControl,
current_node: Media,
index: Arc<AtomicUsize>,
}
impl FolderSource {
pub fn new(
config: &PlayoutConfig,
filter_chain: Option<Arc<Mutex<Vec<String>>>>,
current_list: Arc<Mutex<Vec<Media>>>,
global_index: Arc<AtomicUsize>,
player_control: &PlayerControl,
) -> Self {
let mut path_list = vec![];
let mut media_list = vec![];
@ -52,11 +52,10 @@ impl FolderSource {
.into_iter()
.flat_map(|e| e.ok())
.filter(|f| f.path().is_file())
.filter(|f| include_file_extension(config, f.path()))
{
if include_file(config.clone(), entry.path()) {
let media = Media::new(0, &entry.path().to_string_lossy(), false);
media_list.push(media);
}
let media = Media::new(0, &entry.path().to_string_lossy(), false);
media_list.push(media);
}
}
@ -81,20 +80,19 @@ impl FolderSource {
index += 1;
}
*current_list.lock().unwrap() = media_list;
*player_control.current_list.lock().unwrap() = media_list;
Self {
config: config.clone(),
filter_chain,
nodes: current_list,
player_control: player_control.clone(),
current_node: Media::new(0, "", false),
index: global_index,
}
}
fn shuffle(&mut self) {
let mut rng = thread_rng();
let mut nodes = self.nodes.lock().unwrap();
let mut nodes = self.player_control.current_list.lock().unwrap();
nodes.shuffle(&mut rng);
@ -104,7 +102,7 @@ impl FolderSource {
}
fn sort(&mut self) {
let mut nodes = self.nodes.lock().unwrap();
let mut nodes = self.player_control.current_list.lock().unwrap();
nodes.sort_by(|d1, d2| d1.source.cmp(&d2.source));
@ -119,15 +117,19 @@ impl Iterator for FolderSource {
type Item = Media;
fn next(&mut self) -> Option<Self::Item> {
if self.index.load(Ordering::SeqCst) < self.nodes.lock().unwrap().len() {
let i = self.index.load(Ordering::SeqCst);
self.current_node = self.nodes.lock().unwrap()[i].clone();
if self.player_control.current_index.load(Ordering::SeqCst)
< self.player_control.current_list.lock().unwrap().len()
{
let i = self.player_control.current_index.load(Ordering::SeqCst);
self.current_node = self.player_control.current_list.lock().unwrap()[i].clone();
self.current_node.add_probe();
self.current_node
.add_filter(&self.config, &self.filter_chain);
self.current_node.begin = Some(get_sec());
self.index.fetch_add(1, Ordering::SeqCst);
self.player_control
.current_index
.fetch_add(1, Ordering::SeqCst);
Some(self.current_node.clone())
} else {
@ -145,15 +147,52 @@ impl Iterator for FolderSource {
self.sort();
}
self.current_node = self.nodes.lock().unwrap()[0].clone();
self.current_node = self.player_control.current_list.lock().unwrap()[0].clone();
self.current_node.add_probe();
self.current_node
.add_filter(&self.config, &self.filter_chain);
self.current_node.begin = Some(get_sec());
self.index.store(1, Ordering::SeqCst);
self.player_control.current_index.store(1, Ordering::SeqCst);
Some(self.current_node.clone())
}
}
}
pub fn fill_filler_list(config: PlayoutConfig, player_control: PlayerControl) {
let mut filler_list = vec![];
if Path::new(&config.storage.filler).is_dir() {
debug!(
"Fill filler list from: <b><magenta>{}</></b>",
config.storage.filler
);
for (index, entry) in WalkDir::new(&config.storage.filler)
.into_iter()
.flat_map(|e| e.ok())
.filter(|f| f.path().is_file())
.filter(|f| include_file_extension(&config, f.path()))
.enumerate()
{
filler_list.push(Media::new(index, &entry.path().to_string_lossy(), false));
}
if config.storage.shuffle {
let mut rng = thread_rng();
filler_list.shuffle(&mut rng);
} else {
filler_list.sort_by(|d1, d2| d1.source.cmp(&d2.source));
}
for (index, item) in filler_list.iter_mut().enumerate() {
item.index = Some(index);
}
} else {
filler_list.push(Media::new(0, &config.storage.filler, false));
}
*player_control.filler_list.lock().unwrap() = filler_list;
}

View File

@ -11,12 +11,11 @@ use std::{
io::Error,
path::Path,
process::exit,
sync::{atomic::AtomicUsize, Arc, Mutex},
};
use simplelog::*;
use super::folder::FolderSource;
use super::{folder::FolderSource, PlayerControl};
use crate::utils::{
get_date_range, json_serializer::JsonPlaylist, time_to_sec, Media, PlayoutConfig,
};
@ -36,8 +35,7 @@ pub fn generate_playlist(
}
}
};
let current_list = Arc::new(Mutex::new(vec![Media::new(0, "", false)]));
let index = Arc::new(AtomicUsize::new(0));
let player_control = PlayerControl::new();
let playlist_root = Path::new(&config.playlist.path);
let mut playlists = vec![];
let mut date_range = vec![];
@ -64,8 +62,8 @@ pub fn generate_playlist(
date_range = get_date_range(&date_range)
}
let media_list = FolderSource::new(config, None, current_list, index);
let list_length = media_list.nodes.lock().unwrap().len();
let media_list = FolderSource::new(config, None, &player_control);
let list_length = media_list.player_control.current_list.lock().unwrap().len();
for date in date_range {
let d: Vec<&str> = date.split('-').collect();
@ -90,7 +88,8 @@ pub fn generate_playlist(
playlist_file.display()
);
let mut filler = Media::new(0, &config.storage.filler_clip, true);
// TODO: handle filler folder
let mut filler = Media::new(0, &config.storage.filler, true);
let filler_length = filler.duration;
let mut length = 0.0;
let mut round = 0;

View File

@ -156,7 +156,7 @@ pub fn validate_playlist(
};
} else {
error!(
"Source on position <yellow>{pos}</> {} not exists: <b><magenta>{}</></b>",
"Source on position <yellow>{pos:0>3}</> {} not exists: <b><magenta>{}</></b>",
sec_to_time(begin),
item.source
);

View File

@ -155,8 +155,6 @@ impl SharedLogger for LogMailer {
}
/// Workaround to remove color information from log
///
/// ToDo: maybe in next version from simplelog this is not necessary anymore.
fn clean_string(text: &str) -> String {
let regex = Regex::new(r"\x1b\[[0-9;]*[mGKF]").unwrap();

View File

@ -238,9 +238,13 @@ impl MediaProbe {
}
}
Err(e) => {
error!(
"Can't read source <b><magenta>{input}</></b> with ffprobe, source not exists or damaged! Error in: {e:?}"
);
if Path::new(input).is_file() {
error!(
"Can't read source <b><magenta>{input}</></b> with ffprobe! Error: {e:?}"
);
} else if !input.is_empty() {
error!("File not exists: <b><magenta>{input}</></b>");
}
MediaProbe {
format: None,
@ -586,7 +590,7 @@ pub fn valid_source(source: &str) -> bool {
/// Check if file can include or has to exclude.
/// For example when a file is on given HLS output path, it should exclude.
/// Or when the file extension is set under storage config it can be include.
pub fn include_file(config: PlayoutConfig, file_path: &Path) -> bool {
pub fn include_file_extension(config: &PlayoutConfig, file_path: &Path) -> bool {
let mut include = false;
if let Some(ext) = file_extension(file_path) {
@ -614,6 +618,7 @@ pub fn include_file(config: PlayoutConfig, file_path: &Path) -> bool {
if let Some(m3u8_path) = config
.out
.output_cmd
.clone()
.unwrap_or_else(|| vec![String::new()])
.iter()
.find(|s| s.contains(".m3u8") && !s.contains("master.m3u8"))
@ -878,6 +883,18 @@ pub fn get_date_range(date_range: &[String]) -> Vec<String> {
range
}
pub fn parse_log_level_filter(s: &str) -> Result<LevelFilter, &'static str> {
match s.to_lowercase().as_str() {
"debug" => Ok(LevelFilter::Debug),
"error" => Ok(LevelFilter::Error),
"info" => Ok(LevelFilter::Info),
"trace" => Ok(LevelFilter::Trace),
"warning" => Ok(LevelFilter::Warn),
"off" => Ok(LevelFilter::Off),
_ => Err("Error level not exists!"),
}
}
pub fn home_dir() -> Option<PathBuf> {
home_dir_inner()
}

7
scripts/task-runner.sh Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/bash
# media object
mObj=$1
# perform a meaningful task
notify-send -u normal "ffplayout" -t 2 -e "Play: $(echo $mObj | jq -r '.current_media.source')"

View File

@ -2,20 +2,21 @@ use std::fs;
use ffplayout::{input::playlist::gen_source, utils::prepare_output_cmd};
use ffplayout_lib::{
utils::{Media, OutputMode::*, PlayoutConfig, ProcessUnit::*},
utils::{Media, OutputMode::*, PlayerControl, PlayoutConfig, ProcessUnit::*},
vec_strings,
};
#[test]
fn video_audio_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.add_logo = true;
let logo_path = fs::canonicalize("./assets/logo.png").unwrap();
config.processing.logo = logo_path.to_string_lossy().to_string();
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let test_filter_cmd =
vec_strings![
@ -36,12 +37,13 @@ fn video_audio_input() {
#[test]
fn video_audio_custom_filter1_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.add_logo = false;
config.processing.custom_filter = "[0:v]gblur=2[c_v_out];[0:a]volume=0.2[c_a_out]".to_string();
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let test_filter_cmd = vec_strings![
"-filter_complex",
@ -61,6 +63,7 @@ fn video_audio_custom_filter1_input() {
#[test]
fn video_audio_custom_filter2_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.add_logo = false;
config.processing.custom_filter =
@ -68,7 +71,7 @@ fn video_audio_custom_filter2_input() {
.to_string();
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let test_filter_cmd = vec_strings![
"-filter_complex",
@ -88,13 +91,14 @@ fn video_audio_custom_filter2_input() {
#[test]
fn video_audio_custom_filter3_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.add_logo = false;
config.processing.custom_filter =
"[v_in];movie=logo.png[l];[v_in][l]overlay[c_v_out];[0:a]volume=0.2[c_a_out]".to_string();
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let test_filter_cmd = vec_strings![
"-filter_complex",
@ -114,12 +118,13 @@ fn video_audio_custom_filter3_input() {
#[test]
fn dual_audio_aevalsrc_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.audio_tracks = 2;
config.processing.add_logo = false;
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let test_filter_cmd =
vec_strings![
@ -140,12 +145,13 @@ fn dual_audio_aevalsrc_input() {
#[test]
fn dual_audio_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.audio_tracks = 2;
config.processing.add_logo = false;
let media_obj = Media::new(0, "./assets/dual_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let test_filter_cmd = vec_strings![
"-filter_complex",
@ -165,13 +171,14 @@ fn dual_audio_input() {
#[test]
fn video_separate_audio_input() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = Stream;
config.processing.audio_tracks = 1;
config.processing.add_logo = false;
let mut media_obj = Media::new(0, "./assets/no_audio.mp4", true);
media_obj.audio = "./assets/audio.mp3".to_string();
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let test_filter_cmd = vec_strings![
"-filter_complex",
@ -1305,6 +1312,7 @@ fn video_audio_text_filter_stream() {
#[test]
fn video_audio_hls() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;
config.text.add_text = false;
@ -1333,7 +1341,7 @@ fn video_audio_hls() {
]);
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let enc_prefix = vec_strings![
"-hide_banner",
@ -1356,7 +1364,7 @@ fn video_audio_hls() {
"-i",
"./assets/with_audio.mp4",
"-filter_complex",
"[0:v:0]scale=1024:576,realtime=speed=1[vout0];[0:a:0]anull[aout0]",
"[0:v:0]scale=1024:576[vout0];[0:a:0]anull[aout0]",
"-map",
"[vout0]",
"-map",
@ -1390,6 +1398,7 @@ fn video_audio_hls() {
#[test]
fn video_audio_sub_meta_hls() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;
config.text.add_text = false;
@ -1422,7 +1431,7 @@ fn video_audio_sub_meta_hls() {
]);
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let enc_prefix = vec_strings![
"-hide_banner",
@ -1445,7 +1454,7 @@ fn video_audio_sub_meta_hls() {
"-i",
"./assets/with_audio.mp4",
"-filter_complex",
"[0:v:0]scale=1024:576,realtime=speed=1[vout0];[0:a:0]anull[aout0]",
"[0:v:0]scale=1024:576[vout0];[0:a:0]anull[aout0]",
"-map",
"[vout0]",
"-map",
@ -1483,6 +1492,7 @@ fn video_audio_sub_meta_hls() {
#[test]
fn video_multi_audio_hls() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;
config.processing.audio_tracks = 2;
@ -1512,7 +1522,7 @@ fn video_multi_audio_hls() {
]);
let media_obj = Media::new(0, "./assets/dual_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let enc_prefix = vec_strings![
"-hide_banner",
@ -1535,7 +1545,7 @@ fn video_multi_audio_hls() {
"-i",
"./assets/dual_audio.mp4",
"-filter_complex",
"[0:v:0]scale=1024:576,realtime=speed=1[vout0];[0:a:0]anull[aout0];[0:a:1]anull[aout1]",
"[0:v:0]scale=1024:576[vout0];[0:a:0]anull[aout0];[0:a:1]anull[aout1]",
"-map",
"[vout0]",
"-map",
@ -1571,6 +1581,7 @@ fn video_multi_audio_hls() {
#[test]
fn multi_video_audio_hls() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;
config.text.add_text = false;
@ -1617,7 +1628,7 @@ fn multi_video_audio_hls() {
]);
let media_obj = Media::new(0, "./assets/with_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let enc_prefix = vec_strings![
"-hide_banner",
@ -1640,7 +1651,7 @@ fn multi_video_audio_hls() {
"-i",
"./assets/with_audio.mp4",
"-filter_complex",
"[0:v:0]scale=1024:576,realtime=speed=1,split=2[v1_out][v2];[v2]scale=w=512:h=288[v2_out];[0:a]asplit=2[a1][a2]",
"[0:v:0]scale=1024:576,split=2[v1_out][v2];[v2]scale=w=512:h=288[v2_out];[0:a]asplit=2[a1][a2]",
"-map",
"[v1_out]",
"-map",
@ -1684,6 +1695,7 @@ fn multi_video_audio_hls() {
#[test]
fn multi_video_multi_audio_hls() {
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
let player_control = PlayerControl::new();
config.out.mode = HLS;
config.processing.add_logo = false;
config.processing.audio_tracks = 2;
@ -1733,7 +1745,7 @@ fn multi_video_multi_audio_hls() {
]);
let media_obj = Media::new(0, "./assets/dual_audio.mp4", true);
let media = gen_source(&config, media_obj, &None, 1);
let media = gen_source(&config, media_obj, &None, &player_control, 1);
let enc_prefix = vec_strings![
"-hide_banner",
@ -1756,7 +1768,7 @@ fn multi_video_multi_audio_hls() {
"-i",
"./assets/dual_audio.mp4",
"-filter_complex",
"[0:v:0]scale=1024:576,realtime=speed=1,split=2[v1_out][v2];[v2]scale=w=512:h=288[v2_out];[0:a:0]anull,asplit=2[a_0_1][a_0_2];[0:a:1]anull,asplit=2[a_1_1][a_1_2]",
"[0:v:0]scale=1024:576,split=2[v1_out][v2];[v2]scale=w=512:h=288[v2_out];[0:a:0]anull,asplit=2[a_0_1][a_0_2];[0:a:1]anull,asplit=2[a_1_1][a_1_2]",
"-map",
"[v1_out]",
"-map",

View File

@ -31,7 +31,7 @@ fn playlist_change_at_midnight() {
config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0);
config.playlist.path = "assets/playlists".into();
config.storage.filler_clip = "assets/with_audio.mp4".into();
config.storage.filler = "assets/with_audio.mp4".into();
config.logging.log_to_file = false;
config.logging.timestamp = false;
config.out.mode = Null;
@ -51,7 +51,7 @@ fn playlist_change_at_midnight() {
thread::spawn(move || timed_stop(28, proc_ctl));
player(&config, play_control, playout_stat.clone(), proc_control);
player(&config, &play_control, playout_stat.clone(), proc_control);
let playlist_date = &*playout_stat.current_date.lock().unwrap();
@ -72,7 +72,7 @@ fn playlist_change_at_six() {
config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0);
config.playlist.path = "assets/playlists".into();
config.storage.filler_clip = "assets/with_audio.mp4".into();
config.storage.filler = "assets/with_audio.mp4".into();
config.logging.log_to_file = false;
config.logging.timestamp = false;
config.out.mode = Null;
@ -92,7 +92,7 @@ fn playlist_change_at_six() {
thread::spawn(move || timed_stop(28, proc_ctl));
player(&config, play_control, playout_stat.clone(), proc_control);
player(&config, &play_control, playout_stat.clone(), proc_control);
let playlist_date = &*playout_stat.current_date.lock().unwrap();