work on playlist generator

This commit is contained in:
jb-alvarado 2022-04-13 17:40:47 +02:00
parent b5a991c9ca
commit 614aa7851c
13 changed files with 212 additions and 64 deletions

View File

@ -44,7 +44,7 @@ lto = true
name = "ffplayout-engine" name = "ffplayout-engine"
priority = "optional" priority = "optional"
section = "net" section = "net"
license-file = ["LICENSE", "3"] license-file = ["LICENSE", "0"]
depends = "" depends = ""
suggests = "ffmpeg" suggests = "ffmpeg"
maintainer-scripts = "package/debian/" maintainer-scripts = "package/debian/"
@ -54,9 +54,8 @@ assets = [
["assets/ffplayout.yml", "/etc/ffplayout/ffplayout.yml", "644"], ["assets/ffplayout.yml", "/etc/ffplayout/ffplayout.yml", "644"],
["assets/logo.png", "/usr/share/ffplayout/logo.png", "644"], ["assets/logo.png", "/usr/share/ffplayout/logo.png", "644"],
["README.md", "/usr/share/doc/ffplayout-engine/README", "644"], ["README.md", "/usr/share/doc/ffplayout-engine/README", "644"],
["package/common/ffplayout-engine.service.preset", "/lib/systemd/system-preset/50-ffplayout-engine.preset", "644"],
] ]
systemd-units = { unit-name = "ffplayout-engine", unit-scripts = "package/common", enable = false } systemd-units = { unit-name = "ffplayout-engine", unit-scripts = "assets", enable = false }
# REHL RPM PACKAGE # REHL RPM PACKAGE
[package.metadata.generate-rpm] [package.metadata.generate-rpm]
@ -65,7 +64,7 @@ license = "GPL-3.0"
assets = [ assets = [
{ source = "target/x86_64-unknown-linux-musl/release/ffplayout", dest = "/usr/bin/ffplayout", mode = "755" }, { source = "target/x86_64-unknown-linux-musl/release/ffplayout", dest = "/usr/bin/ffplayout", mode = "755" },
{ source = "assets/ffplayout.yml", dest = "/etc/ffplayout/ffplayout.yml", mode = "644" }, { source = "assets/ffplayout.yml", dest = "/etc/ffplayout/ffplayout.yml", mode = "644" },
{ source = "package/common/ffplayout-engine.service", dest = "/lib/systemd/system/ffplayout-engine.service", mode = "644" }, { source = "assets/ffplayout-engine.service", dest = "/lib/systemd/system/ffplayout-engine.service", mode = "644" },
{ source = "README.md", dest = "/usr/share/doc/ffplayout-engine/README", mode = "644", doc = true }, { source = "README.md", dest = "/usr/share/doc/ffplayout-engine/README", mode = "644", doc = true },
{ source = "LICENSE", dest = "/usr/share/doc/ffplayout-engine/LICENSE", mode = "644" }, { source = "LICENSE", dest = "/usr/share/doc/ffplayout-engine/LICENSE", mode = "644" },
{ source = "assets/logo.png", dest = "/usr/share/ffplayout/logo.png", mode = "644" }, { source = "assets/logo.png", dest = "/usr/share/ffplayout/logo.png", mode = "644" },

View File

@ -44,12 +44,10 @@ echo "Create debian package"
echo "" echo ""
cargo deb --target=x86_64-unknown-linux-musl cargo deb --target=x86_64-unknown-linux-musl
mv ./target/x86_64-unknown-linux-musl/debian/ffplayout-engine_${version}_amd64.deb . mv ./target/x86_64-unknown-linux-musl/debian/ffplayout-engine_${version}_amd64.deb .
echo "Create rhel package" echo "Create rhel package"
echo "" echo ""
cargo generate-rpm --target=x86_64-unknown-linux-musl cargo generate-rpm --target=x86_64-unknown-linux-musl
mv ./target/x86_64-unknown-linux-musl/generate-rpm/ffplayout-engine-${version}-1.x86_64.rpm . mv ./target/x86_64-unknown-linux-musl/generate-rpm/ffplayout-engine-${version}-1.x86_64.rpm .

View File

@ -1 +0,0 @@
disable ffplayout-engine.service

View File

@ -174,10 +174,8 @@ fn overlay(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) {
fn extend_video(node: &mut Media, chain: &mut Filters) { fn extend_video(node: &mut Media, chain: &mut Filters) {
let video_streams = node.probe.clone().unwrap().video_streams.unwrap(); let video_streams = node.probe.clone().unwrap().video_streams.unwrap();
if video_streams.len() > 0 { if video_streams.len() > 0 {
let video_duration = &video_streams[0].duration; if let Some(duration) = &video_streams[0].duration {
let duration_float = duration.clone().parse::<f64>().unwrap();
if video_duration.is_some() {
let duration_float = video_duration.clone().unwrap().parse::<f64>().unwrap();
if node.out - node.seek > duration_float - node.seek + 0.1 { if node.out - node.seek > duration_float - node.seek + 0.1 {
chain.add_filter( chain.add_filter(
@ -226,10 +224,8 @@ fn add_audio(node: &mut Media, chain: &mut Filters) {
fn extend_audio(node: &mut Media, chain: &mut Filters) { fn extend_audio(node: &mut Media, chain: &mut Filters) {
let audio_streams = node.probe.clone().unwrap().audio_streams.unwrap(); let audio_streams = node.probe.clone().unwrap().audio_streams.unwrap();
if audio_streams.len() > 0 { if audio_streams.len() > 0 {
let audio_duration = &audio_streams[0].duration; if let Some(duration) = &audio_streams[0].duration {
let duration_float = duration.clone().parse::<f64>().unwrap();
if audio_duration.is_some() {
let duration_float = audio_duration.clone().unwrap().parse::<f64>().unwrap();
if node.out - node.seek > duration_float - node.seek + 0.1 { if node.out - node.seek > duration_float - node.seek + 0.1 {
chain.add_filter( chain.add_filter(
@ -322,15 +318,13 @@ pub fn filter_chains(node: &mut Media) -> Vec<String> {
let mut audio_map = "1:a".to_string(); let mut audio_map = "1:a".to_string();
filters.audio_map = Some(audio_map); filters.audio_map = Some(audio_map);
if node.probe.is_some() { if let Some(probe) = node.probe.clone() {
let probe = node.probe.clone(); if probe.audio_streams.is_some() {
if node.probe.as_ref().unwrap().audio_streams.is_some() {
audio_map = "0:a".to_string(); audio_map = "0:a".to_string();
filters.audio_map = Some(audio_map); filters.audio_map = Some(audio_map);
} }
let v_stream = &probe.unwrap().video_streams.unwrap()[0]; let v_stream = &probe.video_streams.unwrap()[0];
let aspect = aspect_calc(v_stream.display_aspect_ratio.clone().unwrap()); let aspect = aspect_calc(v_stream.display_aspect_ratio.clone().unwrap());
let frame_per_sec = fps_calc(v_stream.r_frame_rate.clone()); let frame_per_sec = fps_calc(v_stream.r_frame_rate.clone());
@ -364,19 +358,19 @@ pub fn filter_chains(node: &mut Media) -> Vec<String> {
let mut filter_str: String = String::new(); let mut filter_str: String = String::new();
let mut filter_map: Vec<String> = vec![]; let mut filter_map: Vec<String> = vec![];
if filters.video_chain.is_some() { if let Some(v_filters) = filters.video_chain {
filter_str.push_str(filters.video_chain.unwrap().as_str()); filter_str.push_str(v_filters.as_str());
filter_str.push_str(filters.video_map.clone().unwrap().as_str()); filter_str.push_str(filters.video_map.clone().unwrap().as_str());
filter_map.append(&mut vec!["-map".to_string(), filters.video_map.unwrap()]); filter_map.append(&mut vec!["-map".to_string(), filters.video_map.unwrap()]);
} else { } else {
filter_map.append(&mut vec!["-map".to_string(), "0:v".to_string()]); filter_map.append(&mut vec!["-map".to_string(), "0:v".to_string()]);
} }
if filters.audio_chain.is_some() { if let Some(a_filters) = filters.audio_chain {
if filter_str.len() > 10 { if filter_str.len() > 10 {
filter_str.push_str(";") filter_str.push_str(";")
} }
filter_str.push_str(filters.audio_chain.unwrap().as_str()); filter_str.push_str(a_filters.as_str());
filter_str.push_str(filters.audio_map.clone().unwrap().as_str()); filter_str.push_str(filters.audio_map.clone().unwrap().as_str());
filter_map.append(&mut vec!["-map".to_string(), filters.audio_map.unwrap()]); filter_map.append(&mut vec!["-map".to_string(), filters.audio_map.unwrap()]);
} else { } else {

View File

@ -121,10 +121,16 @@ impl Iterator for Source {
Some(self.current_node.clone()) Some(self.current_node.clone())
} else { } else {
if self.config.storage.shuffle { if self.config.storage.shuffle {
info!("Shuffle files"); if self.config.general.generate.is_none() {
info!("Shuffle files");
}
self.shuffle(); self.shuffle();
} else { } else {
info!("Sort files"); if self.config.general.generate.is_none() {
info!("Sort files");
}
self.sort(); self.sort();
} }

View File

@ -9,7 +9,7 @@ use simplelog::*;
use tokio::runtime::Handle; use tokio::runtime::Handle;
use crate::utils::{ use crate::utils::{
check_sync, gen_dummy, get_delta, get_sec, is_close, json_reader::read_json, modified_time, check_sync, gen_dummy, get_delta, get_sec, is_close, json_serializer::read_json, modified_time,
seek_and_length, GlobalConfig, Media, PlayoutStatus, DUMMY_LEN, seek_and_length, GlobalConfig, Media, PlayoutStatus, DUMMY_LEN,
}; };

View File

@ -3,6 +3,7 @@ extern crate simplelog;
use std::{ use std::{
path::PathBuf, path::PathBuf,
process::exit,
{fs, fs::File}, {fs, fs::File},
}; };
@ -19,8 +20,8 @@ mod utils;
use crate::output::{player, write_hls}; use crate::output::{player, write_hls};
use crate::utils::{ use crate::utils::{
init_config, init_logging, validate_ffmpeg, GlobalConfig, PlayerControl, PlayoutStatus, generate_playlist, init_config, init_logging, validate_ffmpeg, GlobalConfig, PlayerControl,
ProcessControl, PlayoutStatus, ProcessControl,
}; };
use rpc::json_rpc_server; use rpc::json_rpc_server;
@ -67,6 +68,13 @@ fn main() {
validate_ffmpeg(); validate_ffmpeg();
if config.general.generate.is_some() {
// generate playlist from given dates
generate_playlist();
exit(0);
}
if config.rpc_server.enable { if config.rpc_server.enable {
rt_handle.spawn(json_rpc_server( rt_handle.spawn(json_rpc_server(
play_control.clone(), play_control.clone(),

View File

@ -5,42 +5,50 @@ use clap::Parser;
about = "ffplayout, Rust based 24/7 playout solution", about = "ffplayout, Rust based 24/7 playout solution",
long_about = None)] long_about = None)]
pub struct Args { pub struct Args {
#[clap(short, long, help = "file path to ffplayout.conf")] #[clap(short, long, help = "File path to ffplayout.conf")]
pub config: Option<String>, pub config: Option<String>,
#[clap(short, long, help = "file path for logging")] #[clap(short, long, help = "File path for logging")]
pub log: Option<String>, pub log: Option<String>,
#[clap(short = 'm', long, help = "playing mode: folder, playlist")] #[clap(
short,
long,
help = "Generate playlist for given date. Date needs format: YYYY-MM-DD. Date-range is possible to, like: 2022-01-01 - 2022-01-10.",
multiple_values=true
)]
pub generate: Option<Vec<String>>,
#[clap(short = 'm', long, help = "Playing mode: folder, playlist")]
pub play_mode: Option<String>, pub play_mode: Option<String>,
#[clap(short, long, help = "play folder content")] #[clap(short, long, help = "Play folder content")]
pub folder: Option<String>, pub folder: Option<String>,
#[clap(short, long, help = "path from playlist")] #[clap(short, long, help = "Path from playlist")]
pub playlist: Option<String>, pub playlist: Option<String>,
#[clap( #[clap(
short, short,
long, long,
help = "start time in 'hh:mm:ss', 'now' for start with first" help = "Start time in 'hh:mm:ss', 'now' for start with first"
)] )]
pub start: Option<String>, pub start: Option<String>,
#[clap( #[clap(
short = 't', short = 't',
long, long,
help = "set length in 'hh:mm:ss', 'none' for no length check" help = "Set length in 'hh:mm:ss', 'none' for no length check"
)] )]
pub length: Option<String>, pub length: Option<String>,
#[clap(short, long, help = "loop playlist infinitely")] #[clap(short, long, help = "Loop playlist infinitely")]
pub infinit: bool, pub infinit: bool,
#[clap(short, long, help = "set output mode: desktop, hls, stream")] #[clap(short, long, help = "Set output mode: desktop, hls, stream")]
pub output: Option<String>, pub output: Option<String>,
#[clap(short, long, help = "set audio volume")] #[clap(short, long, help = "Set audio volume")]
pub volume: Option<f64>, pub volume: Option<f64>,
} }

View File

@ -29,6 +29,7 @@ pub struct GlobalConfig {
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct General { pub struct General {
pub stop_threshold: f64, pub stop_threshold: f64,
pub generate: Option<Vec<String>>,
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub stat_file: String, pub stat_file: String,
@ -138,8 +139,8 @@ impl GlobalConfig {
Err(_) => PathBuf::from("./ffplayout.yml"), Err(_) => PathBuf::from("./ffplayout.yml"),
}; };
if args.config.is_some() { if let Some(cfg) = args.config {
config_path = PathBuf::from(args.config.unwrap()); config_path = PathBuf::from(cfg);
} else if Path::new("/etc/ffplayout/ffplayout.yml").is_file() { } else if Path::new("/etc/ffplayout/ffplayout.yml").is_file() {
config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml"); config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml");
} }
@ -157,6 +158,7 @@ impl GlobalConfig {
let mut config: GlobalConfig = let mut config: GlobalConfig =
serde_yaml::from_reader(f).expect("Could not read config file."); serde_yaml::from_reader(f).expect("Could not read config file.");
config.general.generate = None;
config.general.stat_file = env::temp_dir() config.general.stat_file = env::temp_dir()
.join("ffplayout_status.json") .join("ffplayout_status.json")
.display() .display()
@ -207,32 +209,36 @@ impl GlobalConfig {
config.out.preview_cmd = split(config.out.preview_param.as_str()); config.out.preview_cmd = split(config.out.preview_param.as_str());
config.out.output_cmd = split(config.out.output_param.as_str()); config.out.output_cmd = split(config.out.output_param.as_str());
if args.log.is_some() { if let Some(gen) = args.generate {
config.logging.log_path = args.log.unwrap(); config.general.generate = Some(gen);
} }
if args.playlist.is_some() { if let Some(log_path) = args.log {
config.playlist.path = args.playlist.unwrap(); config.logging.log_path = log_path;
} }
if args.play_mode.is_some() { if let Some(playlist) = args.playlist {
config.processing.mode = args.play_mode.unwrap(); config.playlist.path = playlist;
} }
if args.folder.is_some() { if let Some(mode) = args.play_mode {
config.storage.path = args.folder.unwrap(); config.processing.mode = mode;
} }
if args.start.is_some() { if let Some(folder) = args.folder {
config.playlist.day_start = args.start.clone().unwrap(); config.storage.path = folder;
config.playlist.start_sec = Some(time_to_sec(&args.start.unwrap()));
} }
if args.length.is_some() { if let Some(start) = args.start {
config.playlist.length = args.length.clone().unwrap(); config.playlist.day_start = start.clone();
config.playlist.start_sec = Some(time_to_sec(&start));
}
if config.playlist.length.contains(":") { if let Some(length) = args.length {
config.playlist.length_sec = Some(time_to_sec(&config.playlist.length)); config.playlist.length = length.clone();
if length.contains(":") {
config.playlist.length_sec = Some(time_to_sec(&length));
} else { } else {
config.playlist.length_sec = Some(86400.0); config.playlist.length_sec = Some(86400.0);
} }
@ -242,12 +248,12 @@ impl GlobalConfig {
config.playlist.infinit = args.infinit; config.playlist.infinit = args.infinit;
} }
if args.output.is_some() { if let Some(output) = args.output {
config.out.mode = args.output.unwrap(); config.out.mode = output;
} }
if args.volume.is_some() { if let Some(volume) = args.volume {
config.processing.volume = args.volume.unwrap(); config.processing.volume = volume;
} }
config config

104
src/utils/generator.rs Normal file
View File

@ -0,0 +1,104 @@
use std::{
process::exit,
sync::{Arc, Mutex},
};
use chrono::{Duration, NaiveDate};
use simplelog::*;
use crate::input::Source;
use crate::utils::{json_serializer::Playlist, GlobalConfig, Media};
fn get_date_range(date_range: &Vec<String>) -> Vec<String> {
let mut range = vec![];
let start;
let end;
match NaiveDate::parse_from_str(&date_range[0], "%Y-%m-%d") {
Ok(s) => {
start = s;
}
Err(_) => {
error!("date format error in: <yellow>{:?}</>", date_range[0]);
exit(1);
}
}
match NaiveDate::parse_from_str(&date_range[2], "%Y-%m-%d") {
Ok(e) => {
end = e;
}
Err(_) => {
error!("date format error in: <yellow>{:?}</>", date_range[2]);
exit(1);
}
}
let duration = end.signed_duration_since(start);
let days = duration.num_days() + 1;
for day in 0..days {
range.push((start + Duration::days(day)).format("%Y-%m-%d").to_string());
}
range
}
pub fn generate_playlist() {
let config = GlobalConfig::global();
let mut date_range = config.general.generate.clone().unwrap();
let total_length = config.playlist.length_sec.unwrap().clone();
let current_list = Arc::new(Mutex::new(vec![Media::new(0, "".to_string(), false)]));
let index = Arc::new(Mutex::new(0));
if date_range.contains(&"-".to_string()) && date_range.len() == 3 {
date_range = get_date_range(&date_range)
}
let media_list = Source::new(current_list, index);
for date in date_range {
info!("Generate playlist for {date}");
let mut filler = Media::new(0, config.storage.filler_clip.clone(), true);
let filler_length = filler.duration.clone();
let mut length = 0.0;
let mut playlist = Playlist {
date,
current_file: None,
start_sec: None,
modified: None,
program: vec![],
};
let mut round = 0;
for item in media_list.clone() {
let duration = item.duration.clone();
if total_length > length + filler_length {
playlist.program.push(item);
length += duration;
} else if filler_length > 0.0 && filler_length > total_length - length {
println!("{filler_length}");
filler.out = filler_length - (total_length - length);
println!("{}", total_length - length);
playlist.program.push(filler);
break;
} else if round == 3 {
println!("break");
println!("length {length}");
println!("total_length {total_length}");
break;
} else {
round += 1;
}
}
println!("{length:?}");
println!("{:?}", playlist.program[playlist.program.len() - 1]);
}
}

View File

@ -15,9 +15,16 @@ pub const DUMMY_LEN: f64 = 60.0;
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Playlist { pub struct Playlist {
pub date: String, pub date: String,
#[serde(skip_serializing, skip_deserializing)]
pub start_sec: Option<f64>, pub start_sec: Option<f64>,
#[serde(skip_serializing, skip_deserializing)]
pub current_file: Option<String>, pub current_file: Option<String>,
#[serde(skip_serializing, skip_deserializing)]
pub modified: Option<String>, pub modified: Option<String>,
pub program: Vec<Media>, pub program: Vec<Media>,
} }
@ -86,8 +93,8 @@ pub fn read_json(
playlist.start_sec = Some(start_sec.clone()); playlist.start_sec = Some(start_sec.clone());
let modify = modified_time(&current_file); let modify = modified_time(&current_file);
if modify.is_some() { if let Some(modi) = modify {
playlist.modified = Some(modify.unwrap().to_string()); playlist.modified = Some(modi.to_string());
} }
for (i, item) in playlist.program.iter_mut().enumerate() { for (i, item) in playlist.program.iter_mut().enumerate() {

View File

@ -20,14 +20,16 @@ use simplelog::*;
mod arg_parse; mod arg_parse;
mod config; mod config;
pub mod controller; pub mod controller;
pub mod json_reader; mod generator;
pub mod json_serializer;
mod json_validate; mod json_validate;
mod logging; mod logging;
pub use arg_parse::get_args; pub use arg_parse::get_args;
pub use config::{init_config, GlobalConfig}; pub use config::{init_config, GlobalConfig};
pub use controller::{PlayerControl, PlayoutStatus, ProcessControl, ProcessUnit::*}; pub use controller::{PlayerControl, PlayoutStatus, ProcessControl, ProcessUnit::*};
pub use json_reader::{read_json, Playlist, DUMMY_LEN}; pub use generator::generate_playlist;
pub use json_serializer::{read_json, Playlist, DUMMY_LEN};
pub use json_validate::validate_playlist; pub use json_validate::validate_playlist;
pub use logging::init_logging; pub use logging::init_logging;
@ -35,19 +37,36 @@ use crate::filter::filter_chains;
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Media { pub struct Media {
#[serde(skip_serializing, skip_deserializing)]
pub begin: Option<f64>, pub begin: Option<f64>,
#[serde(skip_serializing, skip_deserializing)]
pub index: Option<usize>, pub index: Option<usize>,
#[serde(rename = "in")] #[serde(rename = "in")]
pub seek: f64, pub seek: f64,
pub out: f64, pub out: f64,
pub duration: f64, pub duration: f64,
#[serde(skip_deserializing)]
pub category: Option<String>, pub category: Option<String>,
pub source: String, pub source: String,
#[serde(skip_serializing, skip_deserializing)]
pub cmd: Option<Vec<String>>, pub cmd: Option<Vec<String>>,
#[serde(skip_serializing, skip_deserializing)]
pub filter: Option<Vec<String>>, pub filter: Option<Vec<String>>,
#[serde(skip_serializing, skip_deserializing)]
pub probe: Option<MediaProbe>, pub probe: Option<MediaProbe>,
#[serde(skip_serializing, skip_deserializing)]
pub last_ad: Option<bool>, pub last_ad: Option<bool>,
#[serde(skip_serializing, skip_deserializing)]
pub next_ad: Option<bool>, pub next_ad: Option<bool>,
#[serde(skip_serializing, skip_deserializing)]
pub process: Option<bool>, pub process: Option<bool>,
} }