commit
ccce51e578
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -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",
|
||||
|
@ -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>"]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
10
docs/stream_copy.md
Normal 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.**
|
@ -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)
|
||||
|
@ -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)]
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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>>
|
||||
}
|
||||
|
@ -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() && ¤t_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, ¤t_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)
|
||||
}
|
||||
|
@ -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...");
|
||||
|
@ -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!");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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() {
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
24
ffplayout-engine/src/utils/task_runner.rs
Normal file
24
ffplayout-engine/src/utils/task_runner.rs
Normal 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
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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
7
scripts/task-runner.sh
Executable 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')"
|
@ -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",
|
||||
|
@ -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();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user