Merge pull request #141 from jb-alvarado/master

fix index out of bounds, integrate infinit loop
This commit is contained in:
jb-alvarado 2022-06-26 23:25:18 +02:00 committed by GitHub
commit 7b42314a93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 189 additions and 26 deletions

6
Cargo.lock generated
View File

@ -1009,7 +1009,7 @@ dependencies = [
[[package]] [[package]]
name = "ffplayout" name = "ffplayout"
version = "0.10.0" version = "0.10.1"
dependencies = [ dependencies = [
"clap", "clap",
"crossbeam-channel 0.5.5", "crossbeam-channel 0.5.5",
@ -1027,7 +1027,7 @@ dependencies = [
[[package]] [[package]]
name = "ffplayout-api" name = "ffplayout-api"
version = "0.3.0" version = "0.3.1"
dependencies = [ dependencies = [
"actix-multipart", "actix-multipart",
"actix-web", "actix-web",
@ -1055,7 +1055,7 @@ dependencies = [
[[package]] [[package]]
name = "ffplayout-lib" name = "ffplayout-lib"
version = "0.10.0" version = "0.10.1"
dependencies = [ dependencies = [
"chrono 0.4.19 (git+https://github.com/chronotope/chrono.git)", "chrono 0.4.19 (git+https://github.com/chronotope/chrono.git)",
"crossbeam-channel 0.5.5", "crossbeam-channel 0.5.5",

3
assets/11-ffplayout Normal file
View File

@ -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

View File

@ -3,7 +3,7 @@ Description=Rest API for ffplayout
After=network.target remote-fs.target After=network.target remote-fs.target
[Service] [Service]
ExecStart= /usr/bin/ffpapi ExecStart=/usr/bin/ffpapi -l 127.0.0.1:8080
ExecReload=/bin/kill -1 $MAINPID ExecReload=/bin/kill -1 $MAINPID
Restart=always Restart=always
RestartSec=1 RestartSec=1

View File

@ -36,7 +36,7 @@ logging:
log_to_file: false log_to_file: false
backup_count: 7 backup_count: 7
local_time: true local_time: true
timestamp: false timestamp: true
log_path: /var/log/ffplayout/ log_path: /var/log/ffplayout/
log_level: DEBUG log_level: DEBUG
ffmpeg_level: error ffmpeg_level: error

View File

@ -138,6 +138,10 @@ Response is in JSON format
- **GET** `/api/control/{id}/media/last/`\ - **GET** `/api/control/{id}/media/last/`\
Response is in JSON format Response is in JSON format
- **POST** `/api/control/{id}/process/`\
JSON Data: `{"command": "<start/stop/restart/status>"}`
Response is in TEXT format
#### Playlist Operations #### Playlist Operations
- **GET** `/api/playlist/{id}/2022-06-20`\ - **GET** `/api/playlist/{id}/2022-06-20`\

View File

@ -4,7 +4,7 @@ description = "Rest API for ffplayout"
license = "GPL-3.0" license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"] authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md" readme = "README.md"
version = "0.3.0" version = "0.3.1"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -17,9 +17,9 @@ use utils::{
routes::{ routes::{
add_preset, add_user, del_playlist, file_browser, gen_playlist, get_playlist, 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, 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, media_current, media_last, media_next, move_rename, patch_settings, process_control,
save_file, save_playlist, send_text_message, update_playout_config, update_preset, remove, reset_playout, save_file, save_playlist, send_text_message, update_playout_config,
update_user, update_preset, update_user,
}, },
run_args, Role, run_args, Role,
}; };
@ -92,6 +92,7 @@ async fn main() -> std::io::Result<()> {
.service(media_current) .service(media_current)
.service(media_next) .service(media_next)
.service(media_last) .service(media_last)
.service(process_control)
.service(get_playlist) .service(get_playlist)
.service(save_playlist) .service(save_playlist)
.service(gen_playlist) .service(gen_playlist)

View File

@ -1,4 +1,4 @@
use std::collections::HashMap; use std::{collections::HashMap, process::Command};
use reqwest::{ use reqwest::{
header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE},
@ -7,7 +7,8 @@ use reqwest::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use simplelog::*; 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)] #[derive(Debug, Deserialize, Serialize, Clone)]
struct RpcObj<T> { struct RpcObj<T> {
@ -44,6 +45,62 @@ impl<T> RpcObj<T> {
} }
} }
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Process {
pub command: String,
}
struct SystemD {
service: String,
cmd: Vec<String>,
}
impl SystemD {
async fn new(id: i64) -> Result<Self, ServiceError> {
let settings = db_get_settings(&id).await?;
Ok(Self {
service: settings.service,
cmd: vec_strings!["systemctl"],
})
}
fn start(mut self) -> Result<String, ServiceError> {
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<String, ServiceError> {
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<String, ServiceError> {
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<String, ServiceError> {
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 { fn create_header(auth: &str) -> HeaderMap {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert( headers.insert(
@ -105,3 +162,15 @@ pub async fn media_info(id: i64, command: String) -> Result<Response, ServiceErr
post_request(id, json_obj).await post_request(id, json_obj).await
} }
pub async fn control_service(id: i64, command: &str) -> Result<String, ServiceError> {
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())),
}
}

View File

@ -59,3 +59,9 @@ impl From<actix_web::error::BlockingError> for ServiceError {
ServiceError::BadRequest(err.to_string()) ServiceError::BadRequest(err.to_string())
} }
} }
impl From<sqlx::Error> for ServiceError {
fn from(err: sqlx::Error) -> ServiceError {
ServiceError::BadRequest(err.to_string())
}
}

View File

@ -56,6 +56,7 @@ async fn create_schema() -> Result<SqliteQueryResult, sqlx::Error> {
preview_url TEXT NOT NULL, preview_url TEXT NOT NULL,
config_path TEXT NOT NULL, config_path TEXT NOT NULL,
extra_extensions TEXT NOT NULL, extra_extensions TEXT NOT NULL,
service TEXT NOT NULL,
UNIQUE(channel_name) UNIQUE(channel_name)
); );
CREATE TABLE IF NOT EXISTS user CREATE TABLE IF NOT EXISTS user
@ -108,9 +109,9 @@ pub async fn db_init() -> Result<&'static str, Box<dyn std::error::Error>> {
('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', ('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'); '24', '4', '#ffffff', '1.0', '1', '#000000@0x80', '4');
INSERT INTO roles(name) VALUES('admin'), ('user'), ('guest'); 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', 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?; sqlx::query(query).bind(secret).execute(&instances).await?;
instances.close().await; instances.close().await;

View File

@ -66,4 +66,5 @@ pub struct Settings {
#[sqlx(default)] #[sqlx(default)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub secret: String, pub secret: String,
pub service: String,
} }

View File

@ -12,7 +12,7 @@ use simplelog::*;
use crate::utils::{ use crate::utils::{
auth::{create_jwt, Claims}, auth::{create_jwt, Claims},
control::{control_state, media_info, send_message}, control::{control_service, control_state, media_info, send_message, Process},
errors::ServiceError, errors::ServiceError,
files::{browser, remove_file_or_folder, rename_file, upload, MoveObject, PathObject}, files::{browser, remove_file_or_folder, rename_file, upload, MoveObject, PathObject},
handles::{ handles::{
@ -352,6 +352,18 @@ pub async fn media_last(id: web::Path<i64>) -> Result<impl Responder, ServiceErr
} }
} }
/// curl -X GET http://localhost:8080/api/control/1/process/
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>'
/// -d '{"command": "start"}'
#[post("/control/{id}/process/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn process_control(
id: web::Path<i64>,
proc: web::Json<Process>,
) -> Result<impl Responder, ServiceError> {
control_service(*id, &proc.command).await
}
/// ---------------------------------------------------------------------------- /// ----------------------------------------------------------------------------
/// ffplayout playlist operations /// ffplayout playlist operations
/// ///

View File

@ -4,7 +4,7 @@ description = "24/7 playout based on rust and ffmpeg"
license = "GPL-3.0" license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"] authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md" readme = "README.md"
version = "0.10.0" version = "0.10.1"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@ -52,6 +52,7 @@ assets = [
"755" "755"
], ],
["../assets/ffpapi.service", "/lib/systemd/system/ffpapi.service", "644"], ["../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/ffplayout.yml", "/etc/ffplayout/ffplayout.yml", "644"],
["../assets/logo.png", "/usr/share/ffplayout/logo.png", "644"], ["../assets/logo.png", "/usr/share/ffplayout/logo.png", "644"],
["../README.md", "/usr/share/doc/ffplayout/README", "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/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/ffpapi.service", dest = "/lib/systemd/system/ffpapi.service", mode = "644" },
{ source = "../assets/ffplayout.service", dest = "/lib/systemd/system/ffplayout.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 = "../README.md", dest = "/usr/share/doc/ffplayout/README", mode = "644", doc = true },
{ source = "../LICENSE", dest = "/usr/share/doc/ffplayout/LICENSE", mode = "644" }, { source = "../LICENSE", dest = "/usr/share/doc/ffplayout/LICENSE", mode = "644" },
{ source = "../assets/logo.png", dest = "/usr/share/ffplayout/logo.png", mode = "644" }, { source = "../assets/logo.png", dest = "/usr/share/ffplayout/logo.png", mode = "644" },

View File

@ -275,19 +275,20 @@ impl Iterator for CurrentProgram {
// On init load, playlist could be not long enough, // On init load, playlist could be not long enough,
// so we check if we can take the next playlist already, // so we check if we can take the next playlist already,
// or we fill the gap with a dummy. // or we fill the gap with a dummy.
let list_length = self.nodes.lock().unwrap().len(); let last_index = self.nodes.lock().unwrap().len() - 1;
self.current_node = self.nodes.lock().unwrap()[list_length - 1].clone(); 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_node = self.nodes.lock().unwrap()[list_length - 1].clone();
let new_length = new_node.begin.unwrap() + new_node.duration; let new_length = new_node.begin.unwrap() + new_node.duration;
self.check_for_next_playlist();
if new_length if new_length
>= self.config.playlist.length_sec.unwrap() >= self.config.playlist.length_sec.unwrap()
+ self.config.playlist.start_sec.unwrap() + self.config.playlist.start_sec.unwrap()
{ {
self.init_clip(); self.init_clip();
} else { } else {
// fill missing length from playlist
let mut current_time = get_sec(); let mut current_time = get_sec();
let (_, total_delta) = get_delta(&self.config, &current_time); let (_, total_delta) = get_delta(&self.config, &current_time);
let mut duration = DUMMY_LEN; let mut duration = DUMMY_LEN;
@ -347,7 +348,8 @@ impl Iterator for CurrentProgram {
let (_, total_delta) = let (_, total_delta) =
get_delta(&self.config, &self.config.playlist.start_sec.unwrap()); 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 && total_delta.abs() > self.config.general.stop_threshold
{ {
// Test if playlist is to early finish, // Test if playlist is to early finish,

View File

@ -4,7 +4,7 @@ description = "Library for ffplayout"
license = "GPL-3.0" license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"] authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md" readme = "README.md"
version = "0.10.0" version = "0.10.1"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -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. /// This is our main playlist object, it holds all necessary information for the current day.
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JsonPlaylist { pub struct JsonPlaylist {
#[serde(default = "default_channel")]
pub channel: String, pub channel: String,
pub date: String, pub date: String,
@ -57,6 +58,10 @@ impl PartialEq for JsonPlaylist {
impl Eq for JsonPlaylist {} impl Eq for JsonPlaylist {}
fn default_channel() -> String {
"Channel 1".to_string()
}
fn set_defaults( fn set_defaults(
mut playlist: JsonPlaylist, mut playlist: JsonPlaylist,
current_file: String, current_file: String,
@ -80,6 +85,54 @@ fn set_defaults(
playlist 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, /// Read json playlist file, fills JsonPlaylist struct and set some extra values,
/// which we need to process. /// which we need to process.
pub fn read_json( pub fn read_json(
@ -131,10 +184,14 @@ pub fn read_json(
validate_playlist(list_clone, is_terminated, config_clone) validate_playlist(list_clone, is_terminated, config_clone)
}); });
if config.playlist.infinit {
return loop_playlist(config, current_file, playlist);
} else {
return set_defaults(playlist, current_file, start_sec); return set_defaults(playlist, current_file, start_sec);
} }
} }
} }
}
} else if playlist_path.is_file() { } else if playlist_path.is_file() {
let f = File::options() let f = File::options()
.read(true) .read(true)
@ -149,10 +206,14 @@ pub fn read_json(
thread::spawn(move || validate_playlist(list_clone, is_terminated, config_clone)); thread::spawn(move || validate_playlist(list_clone, is_terminated, config_clone));
if config.playlist.infinit {
return loop_playlist(config, current_file, playlist);
} else {
return set_defaults(playlist, current_file, start_sec); return set_defaults(playlist, current_file, start_sec);
} }
}
error!("Read playlist error, on: <b><magenta>{current_file}</></b>"); error!("Playlist <b><magenta>{current_file}</></b> not exist!");
JsonPlaylist::new(date, start_sec) JsonPlaylist::new(date, start_sec)
} }

View File

@ -53,7 +53,7 @@ pub fn validate_playlist(
begin += item.out - item.seek; begin += item.out - item.seek;
} }
if length > begin + 1.0 { if !config.playlist.infinit && length > begin + 1.0 {
error!( error!(
"Playlist from <yellow>{date}</> not long enough, <yellow>{}</> needed!", "Playlist from <yellow>{date}</> not long enough, <yellow>{}</> needed!",
sec_to_time(length - begin), sec_to_time(length - begin),

View File

@ -48,6 +48,7 @@ pub struct Media {
pub out: f64, pub out: f64,
pub duration: f64, pub duration: f64,
#[serde(default)]
pub category: String, pub category: String,
pub source: String, pub source: String,