From 933d7a9065e3ad617fe26f3d127c7ea6c23779b8 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Thu, 13 Jun 2024 09:29:55 +0200 Subject: [PATCH] add env variables to clap arguments, add paths to global table and change config paths to relative --- ffplayout/Cargo.toml | 2 +- ffplayout/src/api/auth.rs | 18 +++-- ffplayout/src/api/routes.rs | 78 ++++++++++---------- ffplayout/src/db/handles.rs | 6 +- ffplayout/src/db/models.rs | 109 ++++++++++++++++++++++++++++ ffplayout/src/main.rs | 6 +- ffplayout/src/sse/routes.rs | 3 +- ffplayout/src/utils/args_parse.rs | 28 ++++++-- ffplayout/src/utils/config.rs | 48 ++++++++++--- ffplayout/src/utils/errors.rs | 6 ++ ffplayout/src/utils/mod.rs | 112 +---------------------------- migrations/00001_create_tables.sql | 18 ++--- 12 files changed, 249 insertions(+), 185 deletions(-) diff --git a/ffplayout/Cargo.toml b/ffplayout/Cargo.toml index c6e51ad3..3b2d68ec 100644 --- a/ffplayout/Cargo.toml +++ b/ffplayout/Cargo.toml @@ -21,7 +21,7 @@ actix-web-httpauth = "0.8" actix-web-lab = "0.20" argon2 = "0.5" chrono = { version = "0.4", default-features = false, features = ["clock", "std", "serde"] } -clap = { version = "4.3", features = ["derive"] } +clap = { version = "4.3", features = ["derive", "env"] } crossbeam-channel = "0.5" derive_more = "0.99" faccess = "0.2" diff --git a/ffplayout/src/api/auth.rs b/ffplayout/src/api/auth.rs index 9c933a68..8edc1e2c 100644 --- a/ffplayout/src/api/auth.rs +++ b/ffplayout/src/api/auth.rs @@ -4,7 +4,10 @@ use chrono::{TimeDelta, Utc}; use jsonwebtoken::{self, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; -use crate::utils::{GlobalSettings, Role}; +use crate::{ + db::models::{GlobalSettings, Role}, + utils::errors::ServiceError, +}; // Token lifetime const JWT_EXPIRATION_DAYS: i64 = 7; @@ -29,17 +32,20 @@ impl Claims { } /// Create a json web token (JWT) -pub fn create_jwt(claims: Claims) -> Result { +pub async fn create_jwt(claims: Claims) -> Result { let config = GlobalSettings::global(); - let encoding_key = EncodingKey::from_secret(config.secret.as_bytes()); - jsonwebtoken::encode(&Header::default(), &claims, &encoding_key) - .map_err(|e| ErrorUnauthorized(e.to_string())) + let encoding_key = EncodingKey::from_secret(config.secret.clone().unwrap().as_bytes()); + Ok(jsonwebtoken::encode( + &Header::default(), + &claims, + &encoding_key, + )?) } /// Decode a json web token (JWT) pub async fn decode_jwt(token: &str) -> Result { let config = GlobalSettings::global(); - let decoding_key = DecodingKey::from_secret(config.secret.as_bytes()); + let decoding_key = DecodingKey::from_secret(config.secret.clone().unwrap().as_bytes()); jsonwebtoken::decode::(token, &decoding_key, &Validation::default()) .map(|data| data.claims) .map_err(|e| ErrorUnauthorized(e.to_string())) diff --git a/ffplayout/src/api/routes.rs b/ffplayout/src/api/routes.rs index 375dc2b0..8c9b9a65 100644 --- a/ffplayout/src/api/routes.rs +++ b/ffplayout/src/api/routes.rs @@ -38,6 +38,7 @@ use serde::{Deserialize, Serialize}; use sqlx::{Pool, Sqlite}; use tokio::fs; +use crate::db::models::Role; use crate::utils::{ channels::{create_channel, delete_channel}, config::{ @@ -52,7 +53,7 @@ use crate::utils::{ }, naive_date_time_from_str, playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist}, - public_path, read_log_file, system, Role, TextFilter, + public_path, read_log_file, system, TextFilter, }; use crate::vec_strings; use crate::{ @@ -164,53 +165,56 @@ struct ProgramItem { #[post("/auth/login/")] pub async fn login(pool: web::Data>, credentials: web::Json) -> impl Responder { let conn = pool.into_inner(); - match handles::select_login(&conn, &credentials.username).await { + let username = credentials.username.clone(); + let password = credentials.password.clone(); + + match handles::select_login(&conn, &username).await { Ok(mut user) => { let role = handles::select_role(&conn, &user.role_id.unwrap_or_default()) .await .unwrap_or(Role::Guest); - web::block(move || { - let pass = user.password.clone(); + let pass = user.password.clone(); + let password_clone = password.clone(); + + user.password = "".into(); + + if web::block(move || { let hash = PasswordHash::new(&pass).unwrap(); - user.password = "".into(); - - if Argon2::default() - .verify_password(credentials.password.as_bytes(), &hash) - .is_ok() - { - let claims = Claims::new(user.id, user.username.clone(), role.clone()); - - if let Ok(token) = create_jwt(claims) { - user.token = Some(token); - }; - - info!("user {} login, with role: {role}", credentials.username); - - web::Json(UserObj { - message: "login correct!".into(), - user: Some(user), - }) - .customize() - .with_status(StatusCode::OK) - } else { - error!("Wrong password for {}!", credentials.username); - - web::Json(UserObj { - message: "Wrong password!".into(), - user: None, - }) - .customize() - .with_status(StatusCode::FORBIDDEN) - } + Argon2::default().verify_password(password_clone.as_bytes(), &hash) }) .await - .unwrap() + .is_ok() + { + let claims = Claims::new(user.id, username.clone(), role.clone()); + + if let Ok(token) = create_jwt(claims).await { + user.token = Some(token); + }; + + info!("user {} login, with role: {role}", username); + + web::Json(UserObj { + message: "login correct!".into(), + user: Some(user), + }) + .customize() + .with_status(StatusCode::OK) + } else { + error!("Wrong password for {username}!"); + + web::Json(UserObj { + message: "Wrong password!".into(), + user: None, + }) + .customize() + .with_status(StatusCode::FORBIDDEN) + } } Err(e) => { - error!("Login {} failed! {e}", credentials.username); + error!("Login {username} failed! {e}"); web::Json(UserObj { - message: format!("Login {} failed!", credentials.username), + message: format!("Login {username} failed!"), user: None, }) .customize() diff --git a/ffplayout/src/db/handles.rs b/ffplayout/src/db/handles.rs index 08ba63dc..93f5f53a 100644 --- a/ffplayout/src/db/handles.rs +++ b/ffplayout/src/db/handles.rs @@ -9,8 +9,8 @@ use sqlx::{sqlite::SqliteQueryResult, Pool, Sqlite}; use tokio::task; use super::models::{AdvancedConfiguration, Configuration}; -use crate::db::models::{Channel, TextPreset, User}; -use crate::utils::{local_utc_offset, GlobalSettings, Role}; +use crate::db::models::{Channel, GlobalSettings, Role, TextPreset, User}; +use crate::utils::local_utc_offset; pub async fn db_migrate(conn: &Pool) -> Result<&'static str, Box> { match sqlx::migrate!("../migrations").run(conn).await { @@ -40,7 +40,7 @@ pub async fn db_migrate(conn: &Pool) -> Result<&'static str, Box) -> Result { - let query = "SELECT secret FROM global WHERE id = 1"; + let query = "SELECT secret, hls_path, playlist_path, storage_path, logging_path FROM global WHERE id = 1"; sqlx::query_as(query).fetch_one(conn).await } diff --git a/ffplayout/src/db/models.rs b/ffplayout/src/db/models.rs index 93a90434..e1b86438 100644 --- a/ffplayout/src/db/models.rs +++ b/ffplayout/src/db/models.rs @@ -1,11 +1,53 @@ +use std::{error::Error, fmt, str::FromStr}; + +use once_cell::sync::OnceCell; use regex::Regex; use serde::{ de::{self, Visitor}, Deserialize, Serialize, }; +use sqlx::{sqlite::SqliteRow, FromRow, Pool, Row, Sqlite}; +use crate::db::handles; use crate::utils::config::PlayoutConfig; +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +pub struct GlobalSettings { + pub secret: Option, + pub hls_path: String, + pub playlist_path: String, + pub storage_path: String, + pub logging_path: String, +} + +impl GlobalSettings { + pub async fn new(conn: &Pool) -> Self { + let global_settings = handles::select_global(conn); + + match global_settings.await { + Ok(g) => g, + Err(_) => GlobalSettings { + secret: None, + hls_path: String::new(), + playlist_path: String::new(), + storage_path: String::new(), + logging_path: String::new(), + }, + } + } + + pub fn global() -> &'static GlobalSettings { + INSTANCE.get().expect("Config is not initialized") + } +} + +static INSTANCE: OnceCell = OnceCell::new(); + +pub async fn init_globales(conn: &Pool) { + let config = GlobalSettings::new(conn).await; + INSTANCE.set(config).unwrap(); +} + #[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] pub struct User { #[sqlx(default)] @@ -45,6 +87,73 @@ impl LoginUser { } } +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub enum Role { + GlobalAdmin, + ChannelAdmin, + User, + Guest, +} + +impl Role { + pub fn set_role(role: &str) -> Self { + match role { + "global_admin" => Role::GlobalAdmin, + "channel_admin" => Role::ChannelAdmin, + "user" => Role::User, + _ => Role::Guest, + } + } +} + +impl FromStr for Role { + type Err = String; + + fn from_str(input: &str) -> Result { + match input { + "global_admin" => Ok(Self::GlobalAdmin), + "channel_admin" => Ok(Self::ChannelAdmin), + "user" => Ok(Self::User), + _ => Ok(Self::Guest), + } + } +} + +impl fmt::Display for Role { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::GlobalAdmin => write!(f, "global_admin"), + Self::ChannelAdmin => write!(f, "channel_admin"), + Self::User => write!(f, "user"), + Self::Guest => write!(f, "guest"), + } + } +} + +impl<'r> sqlx::decode::Decode<'r, ::sqlx::Sqlite> for Role +where + &'r str: sqlx::decode::Decode<'r, sqlx::Sqlite>, +{ + fn decode( + value: >::ValueRef, + ) -> Result> { + let value = <&str as sqlx::decode::Decode>::decode(value)?; + + Ok(value.parse()?) + } +} + +impl FromRow<'_, SqliteRow> for Role { + fn from_row(row: &SqliteRow) -> sqlx::Result { + match row.get("name") { + "global_admin" => Ok(Self::GlobalAdmin), + "channel_admin" => Ok(Self::ChannelAdmin), + "user" => Ok(Self::User), + _ => Ok(Self::Guest), + } + } +} + #[derive(Debug, Deserialize, Serialize, Clone, sqlx::FromRow)] pub struct TextPreset { #[sqlx(default)] diff --git a/ffplayout/src/main.rs b/ffplayout/src/main.rs index 8efdd889..dc0ecf1d 100644 --- a/ffplayout/src/main.rs +++ b/ffplayout/src/main.rs @@ -21,12 +21,14 @@ use path_clean::PathClean; use ffplayout::{ api::{auth, routes::*}, - db::{db_pool, handles, models::LoginUser}, + db::{ + db_pool, handles, + models::{init_globales, LoginUser}, + }, player::controller::{ChannelController, ChannelManager}, sse::{broadcast::Broadcaster, routes::*, AuthState}, utils::{ config::PlayoutConfig, - init_globales, logging::{init_logging, MailQueue}, run_args, }, diff --git a/ffplayout/src/sse/routes.rs b/ffplayout/src/sse/routes.rs index aa06409f..f9a83f32 100644 --- a/ffplayout/src/sse/routes.rs +++ b/ffplayout/src/sse/routes.rs @@ -5,9 +5,10 @@ use actix_web_grants::proc_macro::protect; use serde::{Deserialize, Serialize}; use super::{check_uuid, prune_uuids, AuthState, UuidData}; +use crate::db::models::Role; use crate::player::controller::ChannelController; use crate::sse::broadcast::Broadcaster; -use crate::utils::{errors::ServiceError, Role}; +use crate::utils::errors::ServiceError; #[derive(Deserialize, Serialize)] struct User { diff --git a/ffplayout/src/utils/args_parse.rs b/ffplayout/src/utils/args_parse.rs index 8b1e2551..9facca65 100644 --- a/ffplayout/src/utils/args_parse.rs +++ b/ffplayout/src/utils/args_parse.rs @@ -10,12 +10,13 @@ pub struct Args { #[clap(short, long, help = "ask for user credentials")] pub ask: bool, - #[clap(long, help = "path to database file")] + #[clap(long, env, help = "path to database file")] pub db: Option, #[clap( short, long, + env, help = "Run channels by ids immediately (works without webserver and frontend, no listening parameter is needed)", num_args = 1.., )] @@ -24,24 +25,37 @@ pub struct Args { #[clap(long, help = "List available channel ids")] pub list_channels: bool, - #[clap(long, help = "path to public files")] + #[clap(long, env, help = "path to public files")] pub public: Option, - #[clap(short, long, help = "Listen on IP:PORT, like: 127.0.0.1:8787")] + #[clap(short, env, long, help = "Listen on IP:PORT, like: 127.0.0.1:8787")] pub listen: Option, - #[clap(long, help = "Keep log file for given days")] + #[clap(long, env, help = "Keep log file for given days")] pub log_backup_count: Option, - #[clap(long, help = "Override logging level: trace, debug, info, warn, error")] + #[clap( + long, + env, + help = "Override logging level: trace, debug, info, warn, error" + )] pub log_level: Option, - #[clap(long, help = "Logging path")] + #[clap(long, env, help = "Logging path")] pub log_path: Option, - #[clap(long, help = "Log to console")] + #[clap(long, env, help = "Log to console")] pub log_to_console: bool, + #[clap(long, env, help = "HLS output path")] + pub hls_path: Option, + + #[clap(long, env, help = "Playlist root path")] + pub playlist_path: Option, + + #[clap(long, env, help = "Storage root path")] + pub storage_path: Option, + #[clap(short, long, help = "domain name for initialization")] pub domain: Option, diff --git a/ffplayout/src/utils/config.rs b/ffplayout/src/utils/config.rs index 05277379..4ab3374a 100644 --- a/ffplayout/src/utils/config.rs +++ b/ffplayout/src/utils/config.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use shlex::split; use sqlx::{Pool, Sqlite}; -use crate::db::{handles, models::Configuration}; +use crate::db::{handles, models}; use crate::utils::{free_tcp_socket, time_to_sec}; use crate::vec_strings; use crate::AdvancedConfig; @@ -151,6 +151,8 @@ pub struct Source { /// This we init ones, when ffplayout is starting and use them globally in the hole program. #[derive(Debug, Default, Clone, Deserialize, Serialize)] pub struct PlayoutConfig { + #[serde(skip_serializing, skip_deserializing)] + pub global: Global, #[serde(skip_serializing, skip_deserializing)] pub advanced: AdvancedConfig, pub general: General, @@ -165,6 +167,25 @@ pub struct PlayoutConfig { pub output: Output, } +#[derive(Debug, Default, Clone, Deserialize, Serialize)] +pub struct Global { + pub hls_path: PathBuf, + pub playlist_path: PathBuf, + pub storage_path: PathBuf, + pub logging_path: PathBuf, +} + +impl Global { + pub fn new(config: &models::GlobalSettings) -> Self { + Self { + hls_path: PathBuf::from(config.hls_path.clone()), + playlist_path: PathBuf::from(config.playlist_path.clone()), + storage_path: PathBuf::from(config.storage_path.clone()), + logging_path: PathBuf::from(config.logging_path.clone()), + } + } +} + #[derive(Debug, Default, Clone, Deserialize, Serialize)] pub struct General { pub help_text: String, @@ -188,7 +209,7 @@ pub struct General { } impl General { - fn new(config: &Configuration) -> Self { + fn new(config: &models::Configuration) -> Self { Self { help_text: config.general_help.clone(), id: config.id, @@ -218,7 +239,7 @@ pub struct Mail { } impl Mail { - fn new(config: &Configuration) -> Self { + fn new(config: &models::Configuration) -> Self { Self { help_text: config.mail_help.clone(), subject: config.subject.clone(), @@ -259,7 +280,7 @@ pub struct Logging { } impl Logging { - fn new(config: &Configuration) -> Self { + fn new(config: &models::Configuration) -> Self { Self { help_text: config.logging_help.clone(), ffmpeg_level: config.ffmpeg_level.clone(), @@ -301,7 +322,7 @@ pub struct Processing { } impl Processing { - fn new(config: &Configuration) -> Self { + fn new(config: &models::Configuration) -> Self { Self { help_text: config.processing_help.clone(), mode: ProcessMode::new(&config.processing_mode.clone()), @@ -338,7 +359,7 @@ pub struct Ingest { } impl Ingest { - fn new(config: &Configuration) -> Self { + fn new(config: &models::Configuration) -> Self { Self { help_text: config.ingest_help.clone(), enable: config.ingest_enable, @@ -363,7 +384,7 @@ pub struct Playlist { } impl Playlist { - fn new(config: &Configuration) -> Self { + fn new(config: &models::Configuration) -> Self { Self { help_text: config.playlist_help.clone(), path: PathBuf::from(config.playlist_path.clone()), @@ -388,7 +409,7 @@ pub struct Storage { } impl Storage { - fn new(config: &Configuration) -> Self { + fn new(config: &models::Configuration) -> Self { Self { help_text: config.storage_help.clone(), path: PathBuf::from(config.storage_path.clone()), @@ -421,7 +442,7 @@ pub struct Text { } impl Text { - fn new(config: &Configuration) -> Self { + fn new(config: &models::Configuration) -> Self { Self { help_text: config.text_help.clone(), add_text: config.add_text, @@ -444,7 +465,7 @@ pub struct Task { } impl Task { - fn new(config: &Configuration) -> Self { + fn new(config: &models::Configuration) -> Self { Self { help_text: config.task_help.clone(), enable: config.task_enable, @@ -467,7 +488,7 @@ pub struct Output { } impl Output { - fn new(config: &Configuration) -> Self { + fn new(config: &models::Configuration) -> Self { Self { help_text: config.output_help.clone(), mode: OutputMode::new(&config.output_mode), @@ -521,6 +542,9 @@ fn default_track_index() -> i32 { impl PlayoutConfig { pub async fn new(pool: &Pool, channel: i32) -> Self { + let global = handles::select_global(pool) + .await + .expect("Can't read globals"); let config = handles::select_configuration(pool, channel) .await .expect("Can't read config"); @@ -528,6 +552,7 @@ impl PlayoutConfig { .await .expect("Can't read advanced config"); + let global = Global::new(&global); let advanced = AdvancedConfig::new(adv_config); let general = General::new(&config); let mail = Mail::new(&config); @@ -644,6 +669,7 @@ impl PlayoutConfig { } Self { + global, advanced, general, mail, diff --git a/ffplayout/src/utils/errors.rs b/ffplayout/src/utils/errors.rs index cf7d2951..1b50a3e3 100644 --- a/ffplayout/src/utils/errors.rs +++ b/ffplayout/src/utils/errors.rs @@ -77,6 +77,12 @@ impl From for ServiceError { } } +impl From for ServiceError { + fn from(err: jsonwebtoken::errors::Error) -> ServiceError { + ServiceError::Unauthorized(err.to_string()) + } +} + impl From for ServiceError { fn from(err: actix_web::error::BlockingError) -> ServiceError { ServiceError::BadRequest(err.to_string()) diff --git a/ffplayout/src/utils/mod.rs b/ffplayout/src/utils/mod.rs index 38029fd3..bc50ff29 100644 --- a/ffplayout/src/utils/mod.rs +++ b/ffplayout/src/utils/mod.rs @@ -1,18 +1,14 @@ use std::{ - env, - error::Error, - fmt, + env, fmt, fs::{self, metadata}, io::{stdin, stdout, Write}, net::TcpListener, path::{Path, PathBuf}, - str::FromStr, }; use chrono::{format::ParseErrorKind, prelude::*}; use faccess::PathExt; use log::*; -use once_cell::sync::OnceCell; use path_clean::PathClean; use rand::Rng; use regex::Regex; @@ -21,9 +17,6 @@ use serde::{ de::{self, Visitor}, Deserialize, Deserializer, Serialize, }; -use sqlx::{sqlite::SqliteRow, FromRow, Pool, Row, Sqlite}; - -use crate::ARGS; pub mod advanced_config; pub mod args_parse; @@ -38,109 +31,10 @@ pub mod playlist; pub mod system; pub mod task_runner; -use crate::db::{ - db_pool, - handles::{insert_user, select_global}, - models::User, -}; +use crate::db::{db_pool, handles::insert_user, models::User}; use crate::player::utils::time_to_sec; use crate::utils::{errors::ServiceError, logging::log_file_path}; - -#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] -pub enum Role { - GlobalAdmin, - ChannelAdmin, - User, - Guest, -} - -impl Role { - pub fn set_role(role: &str) -> Self { - match role { - "global_admin" => Role::GlobalAdmin, - "channel_admin" => Role::ChannelAdmin, - "user" => Role::User, - _ => Role::Guest, - } - } -} - -impl FromStr for Role { - type Err = String; - - fn from_str(input: &str) -> Result { - match input { - "global_admin" => Ok(Self::GlobalAdmin), - "channel_admin" => Ok(Self::ChannelAdmin), - "user" => Ok(Self::User), - _ => Ok(Self::Guest), - } - } -} - -impl fmt::Display for Role { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Self::GlobalAdmin => write!(f, "global_admin"), - Self::ChannelAdmin => write!(f, "channel_admin"), - Self::User => write!(f, "user"), - Self::Guest => write!(f, "guest"), - } - } -} - -impl<'r> sqlx::decode::Decode<'r, ::sqlx::Sqlite> for Role -where - &'r str: sqlx::decode::Decode<'r, sqlx::Sqlite>, -{ - fn decode( - value: >::ValueRef, - ) -> Result> { - let value = <&str as sqlx::decode::Decode>::decode(value)?; - - Ok(value.parse()?) - } -} - -impl FromRow<'_, SqliteRow> for Role { - fn from_row(row: &SqliteRow) -> sqlx::Result { - match row.get("name") { - "global_admin" => Ok(Self::GlobalAdmin), - "channel_admin" => Ok(Self::ChannelAdmin), - "user" => Ok(Self::User), - _ => Ok(Self::Guest), - } - } -} - -#[derive(Debug, sqlx::FromRow)] -pub struct GlobalSettings { - pub secret: String, -} - -impl GlobalSettings { - async fn new(conn: &Pool) -> Self { - let global_settings = select_global(conn); - - match global_settings.await { - Ok(g) => g, - Err(_) => GlobalSettings { - secret: String::new(), - }, - } - } - - pub fn global() -> &'static GlobalSettings { - INSTANCE.get().expect("Config is not initialized") - } -} - -static INSTANCE: OnceCell = OnceCell::new(); - -pub async fn init_globales(conn: &Pool) { - let config = GlobalSettings::new(conn).await; - INSTANCE.set(config).unwrap(); -} +use crate::ARGS; #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct TextFilter { diff --git a/migrations/00001_create_tables.sql b/migrations/00001_create_tables.sql index 693a1a60..ffbded59 100644 --- a/migrations/00001_create_tables.sql +++ b/migrations/00001_create_tables.sql @@ -5,6 +5,10 @@ CREATE TABLE global ( id INTEGER PRIMARY KEY AUTOINCREMENT, secret TEXT NOT NULL, + hls_path TEXT NOT NULL DEFAULT "/usr/share/ffplayout/public", + playlist_path TEXT NOT NULL DEFAULT "/var/lib/ffplayout/playlists", + storage_path TEXT NOT NULL DEFAULT "/var/lib/ffplayout/tv-media", + logging_path TEXT NOT NULL DEFAULT "/var/log/ffplayout", UNIQUE (secret) ); @@ -78,7 +82,7 @@ CREATE TABLE ingest_level TEXT NOT NULL DEFAULT "ERROR", detect_silence INTEGER NOT NULL DEFAULT 1, ignore_lines TEXT NOT NULL DEFAULT "P sub_mb_type 4 out of range at;error while decoding MB;negative number of zero coeffs at;out of range intra chroma pred mode;non-existing SPS 0 referenced in buffering period", - processing_help TEXT NOT NULL DEFAULT "Default processing for all clips, to have them unique. Mode can be playlist or folder.\n'aspect' must be a float number.'logo' is only used if the path exist.\n'logo_scale' scale the logo to target size, leave it blank when no scaling is needed, format is 'width:height', for example '100:-1' for proportional scaling. With 'logo_opacity' logo can become transparent.\nWith 'audio_tracks' it is possible to configure how many audio tracks should be processed.\n'audio_channels' can be use, if audio has more channels then only stereo.\nWith 'logo_position' in format 'x:y' you set the logo position.\nWith 'custom_filter' it is possible, to apply further filters. The filter outputs should end with [c_v_out] for video filter, and [c_a_out] for audio filter.", + processing_help TEXT NOT NULL DEFAULT "Default processing for all clips, to have them unique. Mode can be playlist or folder.\n'aspect' must be a float number.'logo' is only used if the path exist, path is relative to your storage folder.\n'logo_scale' scale the logo to target size, leave it blank when no scaling is needed, format is 'width:height', for example '100:-1' for proportional scaling. With 'logo_opacity' logo can become transparent.\nWith 'audio_tracks' it is possible to configure how many audio tracks should be processed.\n'audio_channels' can be use, if audio has more channels then only stereo.\nWith 'logo_position' in format 'x:y' you set the logo position.\nWith 'custom_filter' it is possible, to apply further filters. The filter outputs should end with [c_v_out] for video filter, and [c_a_out] for audio filter.", processing_mode TEXT NOT NULL DEFAULT "playlist", audio_only INTEGER NOT NULL DEFAULT 0, copy_audio INTEGER NOT NULL DEFAULT 0, @@ -88,7 +92,7 @@ CREATE TABLE aspect REAL NOT NULL DEFAULT 1.778, fps REAL NOT NULL DEFAULT 25.0, add_logo INTEGER NOT NULL DEFAULT 1, - logo TEXT NOT NULL DEFAULT "/usr/share/ffplayout/logo.png", + logo TEXT NOT NULL DEFAULT "graphics/logo.png", logo_scale TEXT NOT NULL DEFAULT "", logo_opacity REAL NOT NULL DEFAULT 0.7, logo_position TEXT NOT NULL DEFAULT "W-w-12:12", @@ -102,19 +106,17 @@ CREATE TABLE ingest_param TEXT NOT NULL DEFAULT "-f live_flv -listen 1 -i rtmp://127.0.0.1:1936/live/stream", ingest_filter TEXT NOT NULL DEFAULT "", playlist_help TEXT NOT NULL DEFAULT "'path' can be a path to a single file, or a directory. For directory put only the root folder, for example '/playlists', subdirectories are read by the program. Subdirectories needs this structure '/playlists/2018/01'.\n'day_start' means at which time the playlist should start, leave day_start blank when playlist should always start at the begin. 'length' represent the target length from playlist, when is blank real length will not consider.\n'infinit: true' works with single playlist file and loops it infinitely.", - playlist_path TEXT NOT NULL DEFAULT "/var/lib/ffplayout/playlists", day_start TEXT NOT NULL DEFAULT "05:59:25", length TEXT NOT NULL DEFAULT "24:00:00", infinit INTEGER NOT NULL DEFAULT 0, storage_help TEXT NOT NULL DEFAULT "'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.\n'extensions' search only files with this extension. Set 'shuffle' to 'true' to pick files randomly.", - storage_path TEXT NOT NULL DEFAULT "/var/lib/ffplayout/tv-media", - filler TEXT NOT NULL DEFAULT "/var/lib/ffplayout/tv-media/filler/filler.mp4", + filler TEXT NOT NULL DEFAULT "filler/filler.mp4", extensions TEXT NOT NULL DEFAULT "mp4;mkv;webm", shuffle INTEGER NOT NULL DEFAULT 1, - text_help TEXT NOT NULL DEFAULT "Overlay text in combination with libzmq for remote text manipulation. On windows fontfile path need to be like this 'C\\:/WINDOWS/fonts/DejaVuSans.ttf'.\n'text_from_filename' activate the extraction from text of a filename. With 'style' you can define the drawtext parameters like position, color, etc. Post Text over API will override this. With 'regex' you can format file names, to get a title from it.", + text_help TEXT NOT NULL DEFAULT "Overlay text in combination with libzmq for remote text manipulation. fontfile is a relative path to your storage folder.\n'text_from_filename' activate the extraction from text of a filename. With 'style' you can define the drawtext parameters like position, color, etc. Post Text over API will override this. With 'regex' you can format file names, to get a title from it.", add_text INTEGER NOT NULL DEFAULT 1, text_from_filename INTEGER NOT NULL DEFAULT 0, - fontfile TEXT NOT NULL DEFAULT "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + fontfile TEXT NOT NULL DEFAULT "fonts/DejaVuSans.ttf", style TEXT NOT NULL DEFAULT "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4", regex TEXT NOT NULL DEFAULT "^.+[/\\](.*)(.mp4|.mkv|.webm)$", task_help TEXT NOT NULL DEFAULT "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.", @@ -122,7 +124,7 @@ CREATE TABLE task_path TEXT NOT NULL DEFAULT "", output_help TEXT NOT NULL DEFAULT "The final playout compression. Set the settings to your needs. 'mode' has the options 'desktop', 'hls', 'null', 'stream'. Use 'stream' and adjust 'output_param:' settings when you want to stream to a rtmp/rtsp/srt/... server.\nIn production don't serve hls playlist with ffplayout, use nginx or another web server!", output_mode TEXT NOT NULL DEFAULT "hls", - output_param TEXT NOT NULL DEFAULT "-c:v libx264 -crf 23 -x264-params keyint=50:min-keyint=25:scenecut=-1 -maxrate 1300k -bufsize 2600k -preset faster -tune zerolatency -profile:v Main -level 3.1 -c:a aac -ar 44100 -b:a 128k -flags +cgop -f hls -hls_time 6 -hls_list_size 600 -hls_flags append_list+delete_segments+omit_endlist -hls_segment_filename /usr/share/ffplayout/public/live/stream-%d.ts /usr/share/ffplayout/public/live/stream.m3u8", + output_param TEXT NOT NULL DEFAULT "-c:v libx264 -crf 23 -x264-params keyint=50:min-keyint=25:scenecut=-1 -maxrate 1300k -bufsize 2600k -preset faster -tune zerolatency -profile:v Main -level 3.1 -c:a aac -ar 44100 -b:a 128k -flags +cgop -f hls -hls_time 6 -hls_list_size 600 -hls_flags append_list+delete_segments+omit_endlist -hls_segment_filename live/stream-%d.ts live/stream.m3u8", FOREIGN KEY (channel_id) REFERENCES channels (id) ON UPDATE CASCADE ON DELETE CASCADE );