diff --git a/Cargo.lock b/Cargo.lock index 004c6ef0..7df33b52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 800ab62f..0a44d18e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 "] diff --git a/README.md b/README.md index b11421ba..d5d94645 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/assets/ffplayout.yml b/assets/ffplayout.yml index 2dece761..7e12c263 100644 --- a/assets/ffplayout.yml +++ b/assets/ffplayout.yml @@ -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 diff --git a/docs/README.md b/docs/README.md index 6f62f1d7..70985615 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 diff --git a/docs/stream_copy.md b/docs/stream_copy.md new file mode 100644 index 00000000..f71c4804 --- /dev/null +++ b/docs/stream_copy.md @@ -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.** diff --git a/ffplayout-api/src/db/handles.rs b/ffplayout-api/src/db/handles.rs index 09c744c4..b037bc02 100644 --- a/ffplayout-api/src/db/handles.rs +++ b/ffplayout-api/src/db/handles.rs @@ -278,7 +278,7 @@ pub async fn update_preset( ) -> Result { 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) diff --git a/ffplayout-api/src/db/models.rs b/ffplayout-api/src/db/models.rs index bc7e5247..a1984625 100644 --- a/ffplayout-api/src/db/models.rs +++ b/ffplayout-api/src/db/models.rs @@ -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 +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(self, value: &str) -> Result { + 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(self, value: u64) -> Result { + Ok(value.to_string()) + } + + fn visit_i64(self, value: i64) -> Result { + Ok(value.to_string()) + } + + fn visit_f64(self, value: f64) -> Result { + Ok(value.to_string()) + } + } + + deserializer.deserialize_any(StringOrNumberVisitor) +} + #[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] pub struct Channel { #[serde(skip_deserializing)] diff --git a/ffplayout-engine/src/input/folder.rs b/ffplayout-engine/src/input/folder.rs index 512cf102..ebf28bad 100644 --- a/ffplayout-engine/src/input/folder.rs +++ b/ffplayout-engine/src/input/folder.rs @@ -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: {old_path:?} to {new_path:?}"); - } 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); diff --git a/ffplayout-engine/src/input/mod.rs b/ffplayout-engine/src/input/mod.rs index cca35600..a69f1148 100644 --- a/ffplayout-engine/src/input/mod.rs +++ b/ffplayout-engine/src/input/mod.rs @@ -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>>, - index: Arc, + player_control: &PlayerControl, playout_stat: PlayoutStatus, is_terminated: Arc, ) -> Box> { @@ -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> } diff --git a/ffplayout-engine/src/input/playlist.rs b/ffplayout-engine/src/input/playlist.rs index e296acc1..4ddc3a7b 100644 --- a/ffplayout-engine/src/input/playlist.rs +++ b/ffplayout-engine/src/input/playlist.rs @@ -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, json_path: Option, json_date: String, - pub nodes: Arc>>, + player_control: PlayerControl, current_node: Media, - index: Arc, is_terminated: Arc, playout_stat: PlayoutStatus, } +/// Prepare a playlist iterator. impl CurrentProgram { pub fn new( config: &PlayoutConfig, playout_stat: PlayoutStatus, is_terminated: Arc, - current_list: Arc>>, - global_index: Arc, + 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: {}", 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 {} not exists!", + "Playlist {} 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>>>, + 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: \"{}\"", node.source); } - warn!("Generate filler with {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::().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::().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 {:.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>>>, + 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>>>, + 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) } diff --git a/ffplayout-engine/src/main.rs b/ffplayout-engine/src/main.rs index 5060c0ed..624066f8 100644 --- a/ffplayout-engine/src/main.rs +++ b/ffplayout-engine/src/main.rs @@ -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..."); diff --git a/ffplayout-engine/src/output/desktop.rs b/ffplayout-engine/src/output/desktop.rs index 0762614c..369007e1 100644 --- a/ffplayout-engine/src/output/desktop.rs +++ b/ffplayout-engine/src/output/desktop.rs @@ -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!"); } } diff --git a/ffplayout-engine/src/output/hls.rs b/ffplayout-engine/src/output/hls.rs index 7e7cb245..98fdb1a7 100644 --- a/ffplayout-engine/src/output/hls.rs +++ b/ffplayout-engine/src/output/hls.rs @@ -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); diff --git a/ffplayout-engine/src/output/mod.rs b/ffplayout-engine/src/output/mod.rs index 96b587e1..1b17a247 100644 --- a/ffplayout-engine/src/output/mod.rs +++ b/ffplayout-engine/src/output/mod.rs @@ -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!( + "{} 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(); diff --git a/ffplayout-engine/src/rpc/server.rs b/ffplayout-engine/src/rpc/server.rs index c3015cc3..2a838757 100644 --- a/ffplayout-engine/src/rpc/server.rs +++ b/ffplayout-engine/src/rpc/server.rs @@ -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, } -fn deserialize_number_or_string<'de, D>(deserializer: D) -> Result, D::Error> +/// Deserialize number or string +pub fn deserialize_number_or_string<'de, D>(deserializer: D) -> Result, 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; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string or a number") + } + + fn visit_str(self, value: &str) -> Result { + 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(self, value: u64) -> Result { + Ok(Some(value.to_string())) + } + + fn visit_i64(self, value: i64) -> Result { + Ok(Some(value.to_string())) + } + + fn visit_f64(self, value: f64) -> Result { + 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 { - 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>> { - 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>> { - 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() { diff --git a/ffplayout-engine/src/utils/arg_parse.rs b/ffplayout-engine/src/utils/arg_parse.rs index c4ea1e28..9d4f7f22 100644 --- a/ffplayout-engine/src/utils/arg_parse.rs +++ b/ffplayout-engine/src/utils/arg_parse.rs @@ -66,6 +66,9 @@ pub struct Args { )] pub length: Option, + #[clap(long, help = "Override logging level")] + pub level: Option, + #[clap(short, long, help = "Loop playlist infinitely")] pub infinit: bool, diff --git a/ffplayout-engine/src/utils/mod.rs b/ffplayout-engine/src/utils/mod.rs index faee87d8..f5ddcc4c 100644 --- a/ffplayout-engine/src/utils/mod.rs +++ b/ffplayout-engine/src/utils/mod.rs @@ -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 { + 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 +} diff --git a/ffplayout-engine/src/utils/task_runner.rs b/ffplayout-engine/src/utils/task_runner.rs new file mode 100644 index 00000000..006c6f0f --- /dev/null +++ b/ffplayout-engine/src/utils/task_runner.rs @@ -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}") + } + } +} diff --git a/ffplayout-frontend b/ffplayout-frontend index 3333aa89..2f323422 160000 --- a/ffplayout-frontend +++ b/ffplayout-frontend @@ -1 +1 @@ -Subproject commit 3333aa8992e6d571bccf6da4beefc3fbb86c56ff +Subproject commit 2f3234221a0aef8e70d9e2b5e9bbfb1fe51921fc diff --git a/lib/src/filter/mod.rs b/lib/src/filter/mod.rs index 867cb4a8..f4e040ff 100644 --- a/lib/src/filter/mod.rs +++ b/lib/src/filter/mod.rs @@ -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, 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 {}", - 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 {}", + 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 { diff --git a/lib/src/filter/v_drawtext.rs b/lib/src/filter/v_drawtext.rs index 014adc61..9944aa54 100644 --- a/lib/src/filter/v_drawtext.rs +++ b/lib/src/filter/v_drawtext.rs @@ -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) diff --git a/lib/src/utils/config.rs b/lib/src/utils/config.rs index a2d1bbcb..c8172432 100644 --- a/lib/src/utils/config.rs +++ b/lib/src/utils/config.rs @@ -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, - pub filler_clip: String, + #[serde(alias = "filler_clip")] + pub filler: String, pub extensions: Vec, 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>, } +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 { - 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 { + 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 diff --git a/lib/src/utils/controller.rs b/lib/src/utils/controller.rs index 518a536a..6c06ce33 100644 --- a/lib/src/utils/controller.rs +++ b/lib/src/utils/controller.rs @@ -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>>, pub current_list: Arc>>, - pub index: Arc, + pub filler_list: Arc>>, + pub current_index: Arc, + pub filler_index: Arc, } 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)), } } } diff --git a/lib/src/utils/folder.rs b/lib/src/utils/folder.rs index 962a5fb4..74a459b7 100644 --- a/lib/src/utils/folder.rs +++ b/lib/src/utils/folder.rs @@ -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>>>, - pub nodes: Arc>>, + pub player_control: PlayerControl, current_node: Media, - index: Arc, } impl FolderSource { pub fn new( config: &PlayoutConfig, filter_chain: Option>>>, - current_list: Arc>>, - global_index: Arc, + 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 { - 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: {}", + 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; +} diff --git a/lib/src/utils/generator.rs b/lib/src/utils/generator.rs index 1053dca4..19bc6492 100644 --- a/lib/src/utils/generator.rs +++ b/lib/src/utils/generator.rs @@ -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; diff --git a/lib/src/utils/json_validate.rs b/lib/src/utils/json_validate.rs index 84a1ad53..3ccab158 100644 --- a/lib/src/utils/json_validate.rs +++ b/lib/src/utils/json_validate.rs @@ -156,7 +156,7 @@ pub fn validate_playlist( }; } else { error!( - "Source on position {pos} {} not exists: {}", + "Source on position {pos:0>3} {} not exists: {}", sec_to_time(begin), item.source ); diff --git a/lib/src/utils/logging.rs b/lib/src/utils/logging.rs index 97e41ca8..f5114679 100644 --- a/lib/src/utils/logging.rs +++ b/lib/src/utils/logging.rs @@ -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(); diff --git a/lib/src/utils/mod.rs b/lib/src/utils/mod.rs index 7d34c284..c96913bb 100644 --- a/lib/src/utils/mod.rs +++ b/lib/src/utils/mod.rs @@ -238,9 +238,13 @@ impl MediaProbe { } } Err(e) => { - error!( - "Can't read source {input} with ffprobe, source not exists or damaged! Error in: {e:?}" - ); + if Path::new(input).is_file() { + error!( + "Can't read source {input} with ffprobe! Error: {e:?}" + ); + } else if !input.is_empty() { + error!("File not exists: {input}"); + } 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 { range } +pub fn parse_log_level_filter(s: &str) -> Result { + 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 { home_dir_inner() } diff --git a/scripts/task-runner.sh b/scripts/task-runner.sh new file mode 100755 index 00000000..1606ad9f --- /dev/null +++ b/scripts/task-runner.sh @@ -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')" diff --git a/tests/src/engine_cmd.rs b/tests/src/engine_cmd.rs index 67faa13f..098bff08 100644 --- a/tests/src/engine_cmd.rs +++ b/tests/src/engine_cmd.rs @@ -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", diff --git a/tests/src/engine_playlist.rs b/tests/src/engine_playlist.rs index 259e651a..c54ae0c0 100644 --- a/tests/src/engine_playlist.rs +++ b/tests/src/engine_playlist.rs @@ -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();