From 3bd18b3fa6ac764239dcc0c28e8bb1879f1e17dc Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 24 Jun 2022 17:41:55 +0200 Subject: [PATCH 1/8] control systemd service --- Cargo.lock | 2 +- assets/11-ffplayout | 3 ++ docs/api.md | 4 ++ ffplayout-api/Cargo.toml | 2 +- ffplayout-api/src/main.rs | 7 +-- ffplayout-api/src/utils/control.rs | 73 +++++++++++++++++++++++++++++- ffplayout-api/src/utils/errors.rs | 6 +++ ffplayout-api/src/utils/handles.rs | 5 +- ffplayout-api/src/utils/models.rs | 1 + ffplayout-api/src/utils/routes.rs | 14 +++++- ffplayout-engine/Cargo.toml | 2 + 11 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 assets/11-ffplayout diff --git a/Cargo.lock b/Cargo.lock index 66c5c6b0..f86aef7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1027,7 +1027,7 @@ dependencies = [ [[package]] name = "ffplayout-api" -version = "0.3.0" +version = "0.3.1" dependencies = [ "actix-multipart", "actix-web", diff --git a/assets/11-ffplayout b/assets/11-ffplayout new file mode 100644 index 00000000..d333da91 --- /dev/null +++ b/assets/11-ffplayout @@ -0,0 +1,3 @@ +# give user www-data permission to control the ffplayout systemd service + +www-data ALL = NOPASSWD: /bin/systemctl start ffplayout.service, /bin/systemctl stop ffplayout.service, /bin/systemctl reload ffplayout.service, /bin/systemctl restart ffplayout.service, /bin/systemctl status ffplayout.service, /bin/systemctl is-active ffplayout.service diff --git a/docs/api.md b/docs/api.md index 6d103dd6..4954b238 100644 --- a/docs/api.md +++ b/docs/api.md @@ -138,6 +138,10 @@ Response is in JSON format - **GET** `/api/control/{id}/media/last/`\ Response is in JSON format +- **POST** `/api/control/{id}/process/`\ +JSON Data: `{"command": ""}` +Response is in TEXT format + #### Playlist Operations - **GET** `/api/playlist/{id}/2022-06-20`\ diff --git a/ffplayout-api/Cargo.toml b/ffplayout-api/Cargo.toml index c625c5b1..452d4efd 100644 --- a/ffplayout-api/Cargo.toml +++ b/ffplayout-api/Cargo.toml @@ -4,7 +4,7 @@ description = "Rest API for ffplayout" license = "GPL-3.0" authors = ["Jonathan Baecker jonbae77@gmail.com"] readme = "README.md" -version = "0.3.0" +version = "0.3.1" edition = "2021" [dependencies] diff --git a/ffplayout-api/src/main.rs b/ffplayout-api/src/main.rs index 64a5daa5..27e59124 100644 --- a/ffplayout-api/src/main.rs +++ b/ffplayout-api/src/main.rs @@ -17,9 +17,9 @@ use utils::{ routes::{ add_preset, add_user, del_playlist, file_browser, gen_playlist, get_playlist, get_playout_config, get_presets, get_settings, jump_to_last, jump_to_next, login, - media_current, media_last, media_next, move_rename, patch_settings, remove, reset_playout, - save_file, save_playlist, send_text_message, update_playout_config, update_preset, - update_user, + media_current, media_last, media_next, move_rename, patch_settings, process_control, + remove, reset_playout, save_file, save_playlist, send_text_message, update_playout_config, + update_preset, update_user, }, run_args, Role, }; @@ -92,6 +92,7 @@ async fn main() -> std::io::Result<()> { .service(media_current) .service(media_next) .service(media_last) + .service(process_control) .service(get_playlist) .service(save_playlist) .service(gen_playlist) diff --git a/ffplayout-api/src/utils/control.rs b/ffplayout-api/src/utils/control.rs index 44804d85..8802b50f 100644 --- a/ffplayout-api/src/utils/control.rs +++ b/ffplayout-api/src/utils/control.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, process::Command}; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, @@ -7,7 +7,8 @@ use reqwest::{ use serde::{Deserialize, Serialize}; use simplelog::*; -use crate::utils::{errors::ServiceError, playout_config}; +use crate::utils::{errors::ServiceError, handles::db_get_settings, playout_config}; +use ffplayout_lib::vec_strings; #[derive(Debug, Deserialize, Serialize, Clone)] struct RpcObj { @@ -44,6 +45,62 @@ impl RpcObj { } } +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Process { + pub command: String, +} + +struct SystemD { + service: String, + cmd: Vec, +} + +impl SystemD { + async fn new(id: i64) -> Result { + let settings = db_get_settings(&id).await?; + + Ok(Self { + service: settings.service, + cmd: vec_strings!["systemctl"], + }) + } + + fn start(mut self) -> Result { + self.cmd + .append(&mut vec!["start".to_string(), self.service]); + + Command::new("sudo").args(self.cmd).spawn()?; + + Ok("Success".to_string()) + } + + fn stop(mut self) -> Result { + self.cmd.append(&mut vec!["stop".to_string(), self.service]); + + Command::new("sudo").args(self.cmd).spawn()?; + + Ok("Success".to_string()) + } + + fn restart(mut self) -> Result { + self.cmd + .append(&mut vec!["restart".to_string(), self.service]); + + Command::new("sudo").args(self.cmd).spawn()?; + + Ok("Success".to_string()) + } + + fn status(mut self) -> Result { + self.cmd + .append(&mut vec!["is-active".to_string(), self.service]); + + let output = Command::new("sudo").args(self.cmd).output()?; + + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } +} + fn create_header(auth: &str) -> HeaderMap { let mut headers = HeaderMap::new(); headers.insert( @@ -105,3 +162,15 @@ pub async fn media_info(id: i64, command: String) -> Result Result { + let system_d = SystemD::new(id).await?; + + match command { + "start" => system_d.start(), + "stop" => system_d.stop(), + "restart" => system_d.restart(), + "status" => system_d.status(), + _ => Err(ServiceError::BadRequest("Command not found!".to_string())), + } +} diff --git a/ffplayout-api/src/utils/errors.rs b/ffplayout-api/src/utils/errors.rs index 2bb20ab9..65294a62 100644 --- a/ffplayout-api/src/utils/errors.rs +++ b/ffplayout-api/src/utils/errors.rs @@ -59,3 +59,9 @@ impl From for ServiceError { ServiceError::BadRequest(err.to_string()) } } + +impl From for ServiceError { + fn from(err: sqlx::Error) -> ServiceError { + ServiceError::BadRequest(err.to_string()) + } +} diff --git a/ffplayout-api/src/utils/handles.rs b/ffplayout-api/src/utils/handles.rs index 9ee0f1f3..fa4e7bbf 100644 --- a/ffplayout-api/src/utils/handles.rs +++ b/ffplayout-api/src/utils/handles.rs @@ -56,6 +56,7 @@ async fn create_schema() -> Result { preview_url TEXT NOT NULL, config_path TEXT NOT NULL, extra_extensions TEXT NOT NULL, + service TEXT NOT NULL, UNIQUE(channel_name) ); CREATE TABLE IF NOT EXISTS user @@ -108,9 +109,9 @@ pub async fn db_init() -> Result<&'static str, Box> { ('Scrolling Text', 'We have a very important announcement to make.', 'ifnot(ld(1),st(1,t));if(lt(t,ld(1)+1),w+4,w-w/12*mod(t-ld(1),12*(w+tw)/w))', '(h-line_h)*0.9', '24', '4', '#ffffff', '1.0', '1', '#000000@0x80', '4'); INSERT INTO roles(name) VALUES('admin'), ('user'), ('guest'); - INSERT INTO settings(channel_name, preview_url, config_path, extra_extensions) + INSERT INTO settings(channel_name, preview_url, config_path, extra_extensions, service) VALUES('Channel 1', 'http://localhost/live/preview.m3u8', - '/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png');"; + '/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png', 'ffplayout.service');"; sqlx::query(query).bind(secret).execute(&instances).await?; instances.close().await; diff --git a/ffplayout-api/src/utils/models.rs b/ffplayout-api/src/utils/models.rs index 1227c82f..31cc5e4f 100644 --- a/ffplayout-api/src/utils/models.rs +++ b/ffplayout-api/src/utils/models.rs @@ -66,4 +66,5 @@ pub struct Settings { #[sqlx(default)] #[serde(skip_serializing, skip_deserializing)] pub secret: String, + pub service: String, } diff --git a/ffplayout-api/src/utils/routes.rs b/ffplayout-api/src/utils/routes.rs index de98af8d..e4d15c86 100644 --- a/ffplayout-api/src/utils/routes.rs +++ b/ffplayout-api/src/utils/routes.rs @@ -12,7 +12,7 @@ use simplelog::*; use crate::utils::{ auth::{create_jwt, Claims}, - control::{control_state, media_info, send_message}, + control::{control_service, control_state, media_info, send_message, Process}, errors::ServiceError, files::{browser, remove_file_or_folder, rename_file, upload, MoveObject, PathObject}, handles::{ @@ -352,6 +352,18 @@ pub async fn media_last(id: web::Path) -> Result' +/// -d '{"command": "start"}' +#[post("/control/{id}/process/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn process_control( + id: web::Path, + proc: web::Json, +) -> Result { + control_service(*id, &proc.command).await +} + /// ---------------------------------------------------------------------------- /// ffplayout playlist operations /// diff --git a/ffplayout-engine/Cargo.toml b/ffplayout-engine/Cargo.toml index 1dfa2f7a..49678bd4 100644 --- a/ffplayout-engine/Cargo.toml +++ b/ffplayout-engine/Cargo.toml @@ -52,6 +52,7 @@ assets = [ "755" ], ["../assets/ffpapi.service", "/lib/systemd/system/ffpapi.service", "644"], + ["../assets/11-ffplayout", "/etc/sudoers.d/11-ffplayout", "644"], ["../assets/ffplayout.yml", "/etc/ffplayout/ffplayout.yml", "644"], ["../assets/logo.png", "/usr/share/ffplayout/logo.png", "644"], ["../README.md", "/usr/share/doc/ffplayout/README", "644"], @@ -69,6 +70,7 @@ assets = [ { source = "../assets/ffplayout.yml", dest = "/etc/ffplayout/ffplayout.yml", mode = "644", config = true }, { source = "../assets/ffpapi.service", dest = "/lib/systemd/system/ffpapi.service", mode = "644" }, { source = "../assets/ffplayout.service", dest = "/lib/systemd/system/ffplayout.service", mode = "644" }, + { source = "../assets/11-ffplayout", dest = "/etc/sudoers.d/11-ffplayout", mode = "644" }, { source = "../README.md", dest = "/usr/share/doc/ffplayout/README", mode = "644", doc = true }, { source = "../LICENSE", dest = "/usr/share/doc/ffplayout/LICENSE", mode = "644" }, { source = "../assets/logo.png", dest = "/usr/share/ffplayout/logo.png", mode = "644" }, From bbc06fafb0cbe1db18092fd3837578d098856fc8 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sat, 25 Jun 2022 22:09:17 +0200 Subject: [PATCH 2/8] make timestamp default on --- assets/ffplayout.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/ffplayout.yml b/assets/ffplayout.yml index db2d89c8..802e06fd 100644 --- a/assets/ffplayout.yml +++ b/assets/ffplayout.yml @@ -36,7 +36,7 @@ logging: log_to_file: false backup_count: 7 local_time: true - timestamp: false + timestamp: true log_path: /var/log/ffplayout/ log_level: DEBUG ffmpeg_level: error From 89226e179c81922f555c3147807d080c031a84a0 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sat, 25 Jun 2022 23:12:20 +0200 Subject: [PATCH 3/8] set default, in case playlist don't includes the category --- lib/src/utils/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/utils/mod.rs b/lib/src/utils/mod.rs index 3cbc0881..cf7fef2b 100644 --- a/lib/src/utils/mod.rs +++ b/lib/src/utils/mod.rs @@ -48,6 +48,7 @@ pub struct Media { pub out: f64, pub duration: f64, + #[serde(default)] pub category: String, pub source: String, From da1799e7040cf3b9945a037c0459954c409df2c4 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sat, 25 Jun 2022 23:12:38 +0200 Subject: [PATCH 4/8] change error message --- lib/src/utils/json_serializer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/utils/json_serializer.rs b/lib/src/utils/json_serializer.rs index 6eeca4a9..66bba237 100644 --- a/lib/src/utils/json_serializer.rs +++ b/lib/src/utils/json_serializer.rs @@ -152,7 +152,7 @@ pub fn read_json( return set_defaults(playlist, current_file, start_sec); } - error!("Read playlist error, on: {current_file}"); + error!("Playlist {current_file} not exist!"); JsonPlaylist::new(date, start_sec) } From 77358fb1407200ff8f6cebc998d766a5ce69d234 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sat, 25 Jun 2022 23:14:21 +0200 Subject: [PATCH 5/8] set last_index TODO: change check_for_next_playlist position/logic, #140 --- Cargo.lock | 2 +- ffplayout-engine/Cargo.toml | 2 +- ffplayout-engine/src/input/playlist.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f86aef7b..ca959803 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1009,7 +1009,7 @@ dependencies = [ [[package]] name = "ffplayout" -version = "0.10.0" +version = "0.10.1" dependencies = [ "clap", "crossbeam-channel 0.5.5", diff --git a/ffplayout-engine/Cargo.toml b/ffplayout-engine/Cargo.toml index 49678bd4..4c4ddcc1 100644 --- a/ffplayout-engine/Cargo.toml +++ b/ffplayout-engine/Cargo.toml @@ -4,7 +4,7 @@ description = "24/7 playout based on rust and ffmpeg" license = "GPL-3.0" authors = ["Jonathan Baecker jonbae77@gmail.com"] readme = "README.md" -version = "0.10.0" +version = "0.10.1" edition = "2021" [dependencies] diff --git a/ffplayout-engine/src/input/playlist.rs b/ffplayout-engine/src/input/playlist.rs index ab8da117..3b382056 100644 --- a/ffplayout-engine/src/input/playlist.rs +++ b/ffplayout-engine/src/input/playlist.rs @@ -275,11 +275,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 list_length = self.nodes.lock().unwrap().len(); - self.current_node = self.nodes.lock().unwrap()[list_length - 1].clone(); + let last_index = self.nodes.lock().unwrap().len() - 1; + self.current_node = self.nodes.lock().unwrap()[last_index].clone(); self.check_for_next_playlist(); - let new_node = self.nodes.lock().unwrap()[list_length - 1].clone(); + let new_node = self.nodes.lock().unwrap()[last_index].clone(); let new_length = new_node.begin.unwrap() + new_node.duration; if new_length From a52be556bf27a099e88e246fd2c17739a0e24575 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sun, 26 Jun 2022 16:45:50 +0200 Subject: [PATCH 6/8] change positon from check_for_next_playlist to prevent index out of bounds, fix #140 --- ffplayout-engine/src/input/playlist.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ffplayout-engine/src/input/playlist.rs b/ffplayout-engine/src/input/playlist.rs index 3b382056..a7b8b51a 100644 --- a/ffplayout-engine/src/input/playlist.rs +++ b/ffplayout-engine/src/input/playlist.rs @@ -277,17 +277,18 @@ impl Iterator for CurrentProgram { // 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(); - self.check_for_next_playlist(); - let new_node = self.nodes.lock().unwrap()[last_index].clone(); let new_length = new_node.begin.unwrap() + new_node.duration; + self.check_for_next_playlist(); + if new_length >= self.config.playlist.length_sec.unwrap() + self.config.playlist.start_sec.unwrap() { self.init_clip(); } else { + // 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; From a28abb3b4c287d1eb28013f857f965a926ea6796 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sun, 26 Jun 2022 23:21:17 +0200 Subject: [PATCH 7/8] add ip:port --- assets/ffpapi.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/ffpapi.service b/assets/ffpapi.service index 3cd83f38..0b7476f9 100644 --- a/assets/ffpapi.service +++ b/assets/ffpapi.service @@ -3,7 +3,7 @@ Description=Rest API for ffplayout After=network.target remote-fs.target [Service] -ExecStart= /usr/bin/ffpapi +ExecStart=/usr/bin/ffpapi -l 127.0.0.1:8080 ExecReload=/bin/kill -1 $MAINPID Restart=always RestartSec=1 From c90fc62a38ee2e51848a69e06cf5e27b24e22a05 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sun, 26 Jun 2022 23:23:14 +0200 Subject: [PATCH 8/8] integrate option infinit for playlist --- Cargo.lock | 2 +- ffplayout-engine/src/input/playlist.rs | 3 +- lib/Cargo.toml | 2 +- lib/src/utils/json_serializer.rs | 65 +++++++++++++++++++++++++- lib/src/utils/json_validate.rs | 2 +- 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca959803..293ed0c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1055,7 +1055,7 @@ dependencies = [ [[package]] name = "ffplayout-lib" -version = "0.10.0" +version = "0.10.1" dependencies = [ "chrono 0.4.19 (git+https://github.com/chronotope/chrono.git)", "crossbeam-channel 0.5.5", diff --git a/ffplayout-engine/src/input/playlist.rs b/ffplayout-engine/src/input/playlist.rs index a7b8b51a..12560ff1 100644 --- a/ffplayout-engine/src/input/playlist.rs +++ b/ffplayout-engine/src/input/playlist.rs @@ -348,7 +348,8 @@ impl Iterator for CurrentProgram { let (_, total_delta) = get_delta(&self.config, &self.config.playlist.start_sec.unwrap()); - if last_playlist == self.json_path + if !self.config.playlist.infinit + && last_playlist == self.json_path && total_delta.abs() > self.config.general.stop_threshold { // Test if playlist is to early finish, diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 7fe8b275..e5097f02 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -4,7 +4,7 @@ description = "Library for ffplayout" license = "GPL-3.0" authors = ["Jonathan Baecker jonbae77@gmail.com"] readme = "README.md" -version = "0.10.0" +version = "0.10.1" edition = "2021" [dependencies] diff --git a/lib/src/utils/json_serializer.rs b/lib/src/utils/json_serializer.rs index 66bba237..489be261 100644 --- a/lib/src/utils/json_serializer.rs +++ b/lib/src/utils/json_serializer.rs @@ -17,6 +17,7 @@ pub const DUMMY_LEN: f64 = 60.0; /// This is our main playlist object, it holds all necessary information for the current day. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct JsonPlaylist { + #[serde(default = "default_channel")] pub channel: String, pub date: String, @@ -57,6 +58,10 @@ impl PartialEq for JsonPlaylist { impl Eq for JsonPlaylist {} +fn default_channel() -> String { + "Channel 1".to_string() +} + fn set_defaults( mut playlist: JsonPlaylist, current_file: String, @@ -80,6 +85,54 @@ fn set_defaults( playlist } +fn loop_playlist( + config: &PlayoutConfig, + current_file: String, + mut playlist: JsonPlaylist, +) -> JsonPlaylist { + let start_sec = config.playlist.start_sec.unwrap(); + let mut begin = start_sec; + let length = config.playlist.length_sec.unwrap(); + let mut program_list = vec![]; + let mut index = 0; + + playlist.current_file = Some(current_file); + playlist.start_sec = Some(start_sec); + + 'program_looper: loop { + for item in playlist.program.iter() { + let media = Media { + index: Some(index), + begin: Some(begin), + seek: item.seek, + out: item.out, + duration: item.duration, + category: item.category.clone(), + source: item.source.clone(), + cmd: item.cmd.clone(), + probe: item.probe.clone(), + process: Some(true), + last_ad: Some(false), + next_ad: Some(false), + filter: Some(vec![]), + }; + + if begin < start_sec + length { + program_list.push(media); + } else { + break 'program_looper; + } + + begin += item.out - item.seek; + index += 1; + } + } + + playlist.program = program_list; + + playlist +} + /// Read json playlist file, fills JsonPlaylist struct and set some extra values, /// which we need to process. pub fn read_json( @@ -131,7 +184,11 @@ pub fn read_json( validate_playlist(list_clone, is_terminated, config_clone) }); - return set_defaults(playlist, current_file, start_sec); + if config.playlist.infinit { + return loop_playlist(config, current_file, playlist); + } else { + return set_defaults(playlist, current_file, start_sec); + } } } } @@ -149,7 +206,11 @@ pub fn read_json( thread::spawn(move || validate_playlist(list_clone, is_terminated, config_clone)); - return set_defaults(playlist, current_file, start_sec); + if config.playlist.infinit { + return loop_playlist(config, current_file, playlist); + } else { + return set_defaults(playlist, current_file, start_sec); + } } error!("Playlist {current_file} not exist!"); diff --git a/lib/src/utils/json_validate.rs b/lib/src/utils/json_validate.rs index 5a359917..f6b47fd9 100644 --- a/lib/src/utils/json_validate.rs +++ b/lib/src/utils/json_validate.rs @@ -53,7 +53,7 @@ pub fn validate_playlist( begin += item.out - item.seek; } - if length > begin + 1.0 { + if !config.playlist.infinit && length > begin + 1.0 { error!( "Playlist from {date} not long enough, {} needed!", sec_to_time(length - begin),