add env variables to clap arguments, add paths to global table and change config paths to relative
This commit is contained in:
parent
1daea32eb9
commit
933d7a9065
@ -21,7 +21,7 @@ actix-web-httpauth = "0.8"
|
|||||||
actix-web-lab = "0.20"
|
actix-web-lab = "0.20"
|
||||||
argon2 = "0.5"
|
argon2 = "0.5"
|
||||||
chrono = { version = "0.4", default-features = false, features = ["clock", "std", "serde"] }
|
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"
|
crossbeam-channel = "0.5"
|
||||||
derive_more = "0.99"
|
derive_more = "0.99"
|
||||||
faccess = "0.2"
|
faccess = "0.2"
|
||||||
|
@ -4,7 +4,10 @@ use chrono::{TimeDelta, Utc};
|
|||||||
use jsonwebtoken::{self, DecodingKey, EncodingKey, Header, Validation};
|
use jsonwebtoken::{self, DecodingKey, EncodingKey, Header, Validation};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::utils::{GlobalSettings, Role};
|
use crate::{
|
||||||
|
db::models::{GlobalSettings, Role},
|
||||||
|
utils::errors::ServiceError,
|
||||||
|
};
|
||||||
|
|
||||||
// Token lifetime
|
// Token lifetime
|
||||||
const JWT_EXPIRATION_DAYS: i64 = 7;
|
const JWT_EXPIRATION_DAYS: i64 = 7;
|
||||||
@ -29,17 +32,20 @@ impl Claims {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a json web token (JWT)
|
/// Create a json web token (JWT)
|
||||||
pub fn create_jwt(claims: Claims) -> Result<String, Error> {
|
pub async fn create_jwt(claims: Claims) -> Result<String, ServiceError> {
|
||||||
let config = GlobalSettings::global();
|
let config = GlobalSettings::global();
|
||||||
let encoding_key = EncodingKey::from_secret(config.secret.as_bytes());
|
let encoding_key = EncodingKey::from_secret(config.secret.clone().unwrap().as_bytes());
|
||||||
jsonwebtoken::encode(&Header::default(), &claims, &encoding_key)
|
Ok(jsonwebtoken::encode(
|
||||||
.map_err(|e| ErrorUnauthorized(e.to_string()))
|
&Header::default(),
|
||||||
|
&claims,
|
||||||
|
&encoding_key,
|
||||||
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode a json web token (JWT)
|
/// Decode a json web token (JWT)
|
||||||
pub async fn decode_jwt(token: &str) -> Result<Claims, Error> {
|
pub async fn decode_jwt(token: &str) -> Result<Claims, Error> {
|
||||||
let config = GlobalSettings::global();
|
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::<Claims>(token, &decoding_key, &Validation::default())
|
jsonwebtoken::decode::<Claims>(token, &decoding_key, &Validation::default())
|
||||||
.map(|data| data.claims)
|
.map(|data| data.claims)
|
||||||
.map_err(|e| ErrorUnauthorized(e.to_string()))
|
.map_err(|e| ErrorUnauthorized(e.to_string()))
|
||||||
|
@ -38,6 +38,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use sqlx::{Pool, Sqlite};
|
use sqlx::{Pool, Sqlite};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
|
use crate::db::models::Role;
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
channels::{create_channel, delete_channel},
|
channels::{create_channel, delete_channel},
|
||||||
config::{
|
config::{
|
||||||
@ -52,7 +53,7 @@ use crate::utils::{
|
|||||||
},
|
},
|
||||||
naive_date_time_from_str,
|
naive_date_time_from_str,
|
||||||
playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist},
|
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::vec_strings;
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -164,28 +165,34 @@ struct ProgramItem {
|
|||||||
#[post("/auth/login/")]
|
#[post("/auth/login/")]
|
||||||
pub async fn login(pool: web::Data<Pool<Sqlite>>, credentials: web::Json<User>) -> impl Responder {
|
pub async fn login(pool: web::Data<Pool<Sqlite>>, credentials: web::Json<User>) -> impl Responder {
|
||||||
let conn = pool.into_inner();
|
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) => {
|
Ok(mut user) => {
|
||||||
let role = handles::select_role(&conn, &user.role_id.unwrap_or_default())
|
let role = handles::select_role(&conn, &user.role_id.unwrap_or_default())
|
||||||
.await
|
.await
|
||||||
.unwrap_or(Role::Guest);
|
.unwrap_or(Role::Guest);
|
||||||
|
|
||||||
web::block(move || {
|
|
||||||
let pass = user.password.clone();
|
let pass = user.password.clone();
|
||||||
let hash = PasswordHash::new(&pass).unwrap();
|
let password_clone = password.clone();
|
||||||
|
|
||||||
user.password = "".into();
|
user.password = "".into();
|
||||||
|
|
||||||
if Argon2::default()
|
if web::block(move || {
|
||||||
.verify_password(credentials.password.as_bytes(), &hash)
|
let hash = PasswordHash::new(&pass).unwrap();
|
||||||
|
Argon2::default().verify_password(password_clone.as_bytes(), &hash)
|
||||||
|
})
|
||||||
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
let claims = Claims::new(user.id, user.username.clone(), role.clone());
|
let claims = Claims::new(user.id, username.clone(), role.clone());
|
||||||
|
|
||||||
if let Ok(token) = create_jwt(claims) {
|
if let Ok(token) = create_jwt(claims).await {
|
||||||
user.token = Some(token);
|
user.token = Some(token);
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("user {} login, with role: {role}", credentials.username);
|
info!("user {} login, with role: {role}", username);
|
||||||
|
|
||||||
web::Json(UserObj {
|
web::Json(UserObj {
|
||||||
message: "login correct!".into(),
|
message: "login correct!".into(),
|
||||||
@ -194,7 +201,7 @@ pub async fn login(pool: web::Data<Pool<Sqlite>>, credentials: web::Json<User>)
|
|||||||
.customize()
|
.customize()
|
||||||
.with_status(StatusCode::OK)
|
.with_status(StatusCode::OK)
|
||||||
} else {
|
} else {
|
||||||
error!("Wrong password for {}!", credentials.username);
|
error!("Wrong password for {username}!");
|
||||||
|
|
||||||
web::Json(UserObj {
|
web::Json(UserObj {
|
||||||
message: "Wrong password!".into(),
|
message: "Wrong password!".into(),
|
||||||
@ -203,14 +210,11 @@ pub async fn login(pool: web::Data<Pool<Sqlite>>, credentials: web::Json<User>)
|
|||||||
.customize()
|
.customize()
|
||||||
.with_status(StatusCode::FORBIDDEN)
|
.with_status(StatusCode::FORBIDDEN)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Login {} failed! {e}", credentials.username);
|
error!("Login {username} failed! {e}");
|
||||||
web::Json(UserObj {
|
web::Json(UserObj {
|
||||||
message: format!("Login {} failed!", credentials.username),
|
message: format!("Login {username} failed!"),
|
||||||
user: None,
|
user: None,
|
||||||
})
|
})
|
||||||
.customize()
|
.customize()
|
||||||
|
@ -9,8 +9,8 @@ use sqlx::{sqlite::SqliteQueryResult, Pool, Sqlite};
|
|||||||
use tokio::task;
|
use tokio::task;
|
||||||
|
|
||||||
use super::models::{AdvancedConfiguration, Configuration};
|
use super::models::{AdvancedConfiguration, Configuration};
|
||||||
use crate::db::models::{Channel, TextPreset, User};
|
use crate::db::models::{Channel, GlobalSettings, Role, TextPreset, User};
|
||||||
use crate::utils::{local_utc_offset, GlobalSettings, Role};
|
use crate::utils::local_utc_offset;
|
||||||
|
|
||||||
pub async fn db_migrate(conn: &Pool<Sqlite>) -> Result<&'static str, Box<dyn std::error::Error>> {
|
pub async fn db_migrate(conn: &Pool<Sqlite>) -> Result<&'static str, Box<dyn std::error::Error>> {
|
||||||
match sqlx::migrate!("../migrations").run(conn).await {
|
match sqlx::migrate!("../migrations").run(conn).await {
|
||||||
@ -40,7 +40,7 @@ pub async fn db_migrate(conn: &Pool<Sqlite>) -> Result<&'static str, Box<dyn std
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn select_global(conn: &Pool<Sqlite>) -> Result<GlobalSettings, sqlx::Error> {
|
pub async fn select_global(conn: &Pool<Sqlite>) -> Result<GlobalSettings, sqlx::Error> {
|
||||||
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
|
sqlx::query_as(query).fetch_one(conn).await
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,53 @@
|
|||||||
|
use std::{error::Error, fmt, str::FromStr};
|
||||||
|
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{
|
use serde::{
|
||||||
de::{self, Visitor},
|
de::{self, Visitor},
|
||||||
Deserialize, Serialize,
|
Deserialize, Serialize,
|
||||||
};
|
};
|
||||||
|
use sqlx::{sqlite::SqliteRow, FromRow, Pool, Row, Sqlite};
|
||||||
|
|
||||||
|
use crate::db::handles;
|
||||||
use crate::utils::config::PlayoutConfig;
|
use crate::utils::config::PlayoutConfig;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)]
|
||||||
|
pub struct GlobalSettings {
|
||||||
|
pub secret: Option<String>,
|
||||||
|
pub hls_path: String,
|
||||||
|
pub playlist_path: String,
|
||||||
|
pub storage_path: String,
|
||||||
|
pub logging_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlobalSettings {
|
||||||
|
pub async fn new(conn: &Pool<Sqlite>) -> 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<GlobalSettings> = OnceCell::new();
|
||||||
|
|
||||||
|
pub async fn init_globales(conn: &Pool<Sqlite>) {
|
||||||
|
let config = GlobalSettings::new(conn).await;
|
||||||
|
INSTANCE.set(config).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)]
|
#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
#[sqlx(default)]
|
#[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<Self, Self::Err> {
|
||||||
|
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: <sqlx::Sqlite as sqlx::database::HasValueRef<'r>>::ValueRef,
|
||||||
|
) -> Result<Role, Box<dyn Error + 'static + Send + Sync>> {
|
||||||
|
let value = <&str as sqlx::decode::Decode<sqlx::Sqlite>>::decode(value)?;
|
||||||
|
|
||||||
|
Ok(value.parse()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRow<'_, SqliteRow> for Role {
|
||||||
|
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
|
||||||
|
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)]
|
#[derive(Debug, Deserialize, Serialize, Clone, sqlx::FromRow)]
|
||||||
pub struct TextPreset {
|
pub struct TextPreset {
|
||||||
#[sqlx(default)]
|
#[sqlx(default)]
|
||||||
|
@ -21,12 +21,14 @@ use path_clean::PathClean;
|
|||||||
|
|
||||||
use ffplayout::{
|
use ffplayout::{
|
||||||
api::{auth, routes::*},
|
api::{auth, routes::*},
|
||||||
db::{db_pool, handles, models::LoginUser},
|
db::{
|
||||||
|
db_pool, handles,
|
||||||
|
models::{init_globales, LoginUser},
|
||||||
|
},
|
||||||
player::controller::{ChannelController, ChannelManager},
|
player::controller::{ChannelController, ChannelManager},
|
||||||
sse::{broadcast::Broadcaster, routes::*, AuthState},
|
sse::{broadcast::Broadcaster, routes::*, AuthState},
|
||||||
utils::{
|
utils::{
|
||||||
config::PlayoutConfig,
|
config::PlayoutConfig,
|
||||||
init_globales,
|
|
||||||
logging::{init_logging, MailQueue},
|
logging::{init_logging, MailQueue},
|
||||||
run_args,
|
run_args,
|
||||||
},
|
},
|
||||||
|
@ -5,9 +5,10 @@ use actix_web_grants::proc_macro::protect;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::{check_uuid, prune_uuids, AuthState, UuidData};
|
use super::{check_uuid, prune_uuids, AuthState, UuidData};
|
||||||
|
use crate::db::models::Role;
|
||||||
use crate::player::controller::ChannelController;
|
use crate::player::controller::ChannelController;
|
||||||
use crate::sse::broadcast::Broadcaster;
|
use crate::sse::broadcast::Broadcaster;
|
||||||
use crate::utils::{errors::ServiceError, Role};
|
use crate::utils::errors::ServiceError;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
struct User {
|
struct User {
|
||||||
|
@ -10,12 +10,13 @@ pub struct Args {
|
|||||||
#[clap(short, long, help = "ask for user credentials")]
|
#[clap(short, long, help = "ask for user credentials")]
|
||||||
pub ask: bool,
|
pub ask: bool,
|
||||||
|
|
||||||
#[clap(long, help = "path to database file")]
|
#[clap(long, env, help = "path to database file")]
|
||||||
pub db: Option<PathBuf>,
|
pub db: Option<PathBuf>,
|
||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
short,
|
short,
|
||||||
long,
|
long,
|
||||||
|
env,
|
||||||
help = "Run channels by ids immediately (works without webserver and frontend, no listening parameter is needed)",
|
help = "Run channels by ids immediately (works without webserver and frontend, no listening parameter is needed)",
|
||||||
num_args = 1..,
|
num_args = 1..,
|
||||||
)]
|
)]
|
||||||
@ -24,24 +25,37 @@ pub struct Args {
|
|||||||
#[clap(long, help = "List available channel ids")]
|
#[clap(long, help = "List available channel ids")]
|
||||||
pub list_channels: bool,
|
pub list_channels: bool,
|
||||||
|
|
||||||
#[clap(long, help = "path to public files")]
|
#[clap(long, env, help = "path to public files")]
|
||||||
pub public: Option<PathBuf>,
|
pub public: Option<PathBuf>,
|
||||||
|
|
||||||
#[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<String>,
|
pub listen: Option<String>,
|
||||||
|
|
||||||
#[clap(long, help = "Keep log file for given days")]
|
#[clap(long, env, help = "Keep log file for given days")]
|
||||||
pub log_backup_count: Option<usize>,
|
pub log_backup_count: Option<usize>,
|
||||||
|
|
||||||
#[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<String>,
|
pub log_level: Option<String>,
|
||||||
|
|
||||||
#[clap(long, help = "Logging path")]
|
#[clap(long, env, help = "Logging path")]
|
||||||
pub log_path: Option<PathBuf>,
|
pub log_path: Option<PathBuf>,
|
||||||
|
|
||||||
#[clap(long, help = "Log to console")]
|
#[clap(long, env, help = "Log to console")]
|
||||||
pub log_to_console: bool,
|
pub log_to_console: bool,
|
||||||
|
|
||||||
|
#[clap(long, env, help = "HLS output path")]
|
||||||
|
pub hls_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[clap(long, env, help = "Playlist root path")]
|
||||||
|
pub playlist_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[clap(long, env, help = "Storage root path")]
|
||||||
|
pub storage_path: Option<PathBuf>,
|
||||||
|
|
||||||
#[clap(short, long, help = "domain name for initialization")]
|
#[clap(short, long, help = "domain name for initialization")]
|
||||||
pub domain: Option<String>,
|
pub domain: Option<String>,
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use shlex::split;
|
use shlex::split;
|
||||||
use sqlx::{Pool, Sqlite};
|
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::utils::{free_tcp_socket, time_to_sec};
|
||||||
use crate::vec_strings;
|
use crate::vec_strings;
|
||||||
use crate::AdvancedConfig;
|
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.
|
/// This we init ones, when ffplayout is starting and use them globally in the hole program.
|
||||||
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
||||||
pub struct PlayoutConfig {
|
pub struct PlayoutConfig {
|
||||||
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
|
pub global: Global,
|
||||||
#[serde(skip_serializing, skip_deserializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
pub advanced: AdvancedConfig,
|
pub advanced: AdvancedConfig,
|
||||||
pub general: General,
|
pub general: General,
|
||||||
@ -165,6 +167,25 @@ pub struct PlayoutConfig {
|
|||||||
pub output: Output,
|
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)]
|
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
||||||
pub struct General {
|
pub struct General {
|
||||||
pub help_text: String,
|
pub help_text: String,
|
||||||
@ -188,7 +209,7 @@ pub struct General {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl General {
|
impl General {
|
||||||
fn new(config: &Configuration) -> Self {
|
fn new(config: &models::Configuration) -> Self {
|
||||||
Self {
|
Self {
|
||||||
help_text: config.general_help.clone(),
|
help_text: config.general_help.clone(),
|
||||||
id: config.id,
|
id: config.id,
|
||||||
@ -218,7 +239,7 @@ pub struct Mail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Mail {
|
impl Mail {
|
||||||
fn new(config: &Configuration) -> Self {
|
fn new(config: &models::Configuration) -> Self {
|
||||||
Self {
|
Self {
|
||||||
help_text: config.mail_help.clone(),
|
help_text: config.mail_help.clone(),
|
||||||
subject: config.subject.clone(),
|
subject: config.subject.clone(),
|
||||||
@ -259,7 +280,7 @@ pub struct Logging {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Logging {
|
impl Logging {
|
||||||
fn new(config: &Configuration) -> Self {
|
fn new(config: &models::Configuration) -> Self {
|
||||||
Self {
|
Self {
|
||||||
help_text: config.logging_help.clone(),
|
help_text: config.logging_help.clone(),
|
||||||
ffmpeg_level: config.ffmpeg_level.clone(),
|
ffmpeg_level: config.ffmpeg_level.clone(),
|
||||||
@ -301,7 +322,7 @@ pub struct Processing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Processing {
|
impl Processing {
|
||||||
fn new(config: &Configuration) -> Self {
|
fn new(config: &models::Configuration) -> Self {
|
||||||
Self {
|
Self {
|
||||||
help_text: config.processing_help.clone(),
|
help_text: config.processing_help.clone(),
|
||||||
mode: ProcessMode::new(&config.processing_mode.clone()),
|
mode: ProcessMode::new(&config.processing_mode.clone()),
|
||||||
@ -338,7 +359,7 @@ pub struct Ingest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Ingest {
|
impl Ingest {
|
||||||
fn new(config: &Configuration) -> Self {
|
fn new(config: &models::Configuration) -> Self {
|
||||||
Self {
|
Self {
|
||||||
help_text: config.ingest_help.clone(),
|
help_text: config.ingest_help.clone(),
|
||||||
enable: config.ingest_enable,
|
enable: config.ingest_enable,
|
||||||
@ -363,7 +384,7 @@ pub struct Playlist {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Playlist {
|
impl Playlist {
|
||||||
fn new(config: &Configuration) -> Self {
|
fn new(config: &models::Configuration) -> Self {
|
||||||
Self {
|
Self {
|
||||||
help_text: config.playlist_help.clone(),
|
help_text: config.playlist_help.clone(),
|
||||||
path: PathBuf::from(config.playlist_path.clone()),
|
path: PathBuf::from(config.playlist_path.clone()),
|
||||||
@ -388,7 +409,7 @@ pub struct Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Storage {
|
impl Storage {
|
||||||
fn new(config: &Configuration) -> Self {
|
fn new(config: &models::Configuration) -> Self {
|
||||||
Self {
|
Self {
|
||||||
help_text: config.storage_help.clone(),
|
help_text: config.storage_help.clone(),
|
||||||
path: PathBuf::from(config.storage_path.clone()),
|
path: PathBuf::from(config.storage_path.clone()),
|
||||||
@ -421,7 +442,7 @@ pub struct Text {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Text {
|
impl Text {
|
||||||
fn new(config: &Configuration) -> Self {
|
fn new(config: &models::Configuration) -> Self {
|
||||||
Self {
|
Self {
|
||||||
help_text: config.text_help.clone(),
|
help_text: config.text_help.clone(),
|
||||||
add_text: config.add_text,
|
add_text: config.add_text,
|
||||||
@ -444,7 +465,7 @@ pub struct Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Task {
|
impl Task {
|
||||||
fn new(config: &Configuration) -> Self {
|
fn new(config: &models::Configuration) -> Self {
|
||||||
Self {
|
Self {
|
||||||
help_text: config.task_help.clone(),
|
help_text: config.task_help.clone(),
|
||||||
enable: config.task_enable,
|
enable: config.task_enable,
|
||||||
@ -467,7 +488,7 @@ pub struct Output {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Output {
|
impl Output {
|
||||||
fn new(config: &Configuration) -> Self {
|
fn new(config: &models::Configuration) -> Self {
|
||||||
Self {
|
Self {
|
||||||
help_text: config.output_help.clone(),
|
help_text: config.output_help.clone(),
|
||||||
mode: OutputMode::new(&config.output_mode),
|
mode: OutputMode::new(&config.output_mode),
|
||||||
@ -521,6 +542,9 @@ fn default_track_index() -> i32 {
|
|||||||
|
|
||||||
impl PlayoutConfig {
|
impl PlayoutConfig {
|
||||||
pub async fn new(pool: &Pool<Sqlite>, channel: i32) -> Self {
|
pub async fn new(pool: &Pool<Sqlite>, channel: i32) -> Self {
|
||||||
|
let global = handles::select_global(pool)
|
||||||
|
.await
|
||||||
|
.expect("Can't read globals");
|
||||||
let config = handles::select_configuration(pool, channel)
|
let config = handles::select_configuration(pool, channel)
|
||||||
.await
|
.await
|
||||||
.expect("Can't read config");
|
.expect("Can't read config");
|
||||||
@ -528,6 +552,7 @@ impl PlayoutConfig {
|
|||||||
.await
|
.await
|
||||||
.expect("Can't read advanced config");
|
.expect("Can't read advanced config");
|
||||||
|
|
||||||
|
let global = Global::new(&global);
|
||||||
let advanced = AdvancedConfig::new(adv_config);
|
let advanced = AdvancedConfig::new(adv_config);
|
||||||
let general = General::new(&config);
|
let general = General::new(&config);
|
||||||
let mail = Mail::new(&config);
|
let mail = Mail::new(&config);
|
||||||
@ -644,6 +669,7 @@ impl PlayoutConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
global,
|
||||||
advanced,
|
advanced,
|
||||||
general,
|
general,
|
||||||
mail,
|
mail,
|
||||||
|
@ -77,6 +77,12 @@ impl From<std::num::ParseIntError> for ServiceError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<jsonwebtoken::errors::Error> for ServiceError {
|
||||||
|
fn from(err: jsonwebtoken::errors::Error) -> ServiceError {
|
||||||
|
ServiceError::Unauthorized(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<actix_web::error::BlockingError> for ServiceError {
|
impl From<actix_web::error::BlockingError> for ServiceError {
|
||||||
fn from(err: actix_web::error::BlockingError) -> ServiceError {
|
fn from(err: actix_web::error::BlockingError) -> ServiceError {
|
||||||
ServiceError::BadRequest(err.to_string())
|
ServiceError::BadRequest(err.to_string())
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
use std::{
|
use std::{
|
||||||
env,
|
env, fmt,
|
||||||
error::Error,
|
|
||||||
fmt,
|
|
||||||
fs::{self, metadata},
|
fs::{self, metadata},
|
||||||
io::{stdin, stdout, Write},
|
io::{stdin, stdout, Write},
|
||||||
net::TcpListener,
|
net::TcpListener,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
str::FromStr,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono::{format::ParseErrorKind, prelude::*};
|
use chrono::{format::ParseErrorKind, prelude::*};
|
||||||
use faccess::PathExt;
|
use faccess::PathExt;
|
||||||
use log::*;
|
use log::*;
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use path_clean::PathClean;
|
use path_clean::PathClean;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
@ -21,9 +17,6 @@ use serde::{
|
|||||||
de::{self, Visitor},
|
de::{self, Visitor},
|
||||||
Deserialize, Deserializer, Serialize,
|
Deserialize, Deserializer, Serialize,
|
||||||
};
|
};
|
||||||
use sqlx::{sqlite::SqliteRow, FromRow, Pool, Row, Sqlite};
|
|
||||||
|
|
||||||
use crate::ARGS;
|
|
||||||
|
|
||||||
pub mod advanced_config;
|
pub mod advanced_config;
|
||||||
pub mod args_parse;
|
pub mod args_parse;
|
||||||
@ -38,109 +31,10 @@ pub mod playlist;
|
|||||||
pub mod system;
|
pub mod system;
|
||||||
pub mod task_runner;
|
pub mod task_runner;
|
||||||
|
|
||||||
use crate::db::{
|
use crate::db::{db_pool, handles::insert_user, models::User};
|
||||||
db_pool,
|
|
||||||
handles::{insert_user, select_global},
|
|
||||||
models::User,
|
|
||||||
};
|
|
||||||
use crate::player::utils::time_to_sec;
|
use crate::player::utils::time_to_sec;
|
||||||
use crate::utils::{errors::ServiceError, logging::log_file_path};
|
use crate::utils::{errors::ServiceError, logging::log_file_path};
|
||||||
|
use crate::ARGS;
|
||||||
#[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<Self, Self::Err> {
|
|
||||||
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: <sqlx::Sqlite as sqlx::database::HasValueRef<'r>>::ValueRef,
|
|
||||||
) -> Result<Role, Box<dyn Error + 'static + Send + Sync>> {
|
|
||||||
let value = <&str as sqlx::decode::Decode<sqlx::Sqlite>>::decode(value)?;
|
|
||||||
|
|
||||||
Ok(value.parse()?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromRow<'_, SqliteRow> for Role {
|
|
||||||
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
|
|
||||||
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<Sqlite>) -> 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<GlobalSettings> = OnceCell::new();
|
|
||||||
|
|
||||||
pub async fn init_globales(conn: &Pool<Sqlite>) {
|
|
||||||
let config = GlobalSettings::new(conn).await;
|
|
||||||
INSTANCE.set(config).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||||
pub struct TextFilter {
|
pub struct TextFilter {
|
||||||
|
@ -5,6 +5,10 @@ CREATE TABLE
|
|||||||
global (
|
global (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
secret TEXT NOT NULL,
|
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)
|
UNIQUE (secret)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -78,7 +82,7 @@ CREATE TABLE
|
|||||||
ingest_level TEXT NOT NULL DEFAULT "ERROR",
|
ingest_level TEXT NOT NULL DEFAULT "ERROR",
|
||||||
detect_silence INTEGER NOT NULL DEFAULT 1,
|
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",
|
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",
|
processing_mode TEXT NOT NULL DEFAULT "playlist",
|
||||||
audio_only INTEGER NOT NULL DEFAULT 0,
|
audio_only INTEGER NOT NULL DEFAULT 0,
|
||||||
copy_audio 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,
|
aspect REAL NOT NULL DEFAULT 1.778,
|
||||||
fps REAL NOT NULL DEFAULT 25.0,
|
fps REAL NOT NULL DEFAULT 25.0,
|
||||||
add_logo INTEGER NOT NULL DEFAULT 1,
|
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_scale TEXT NOT NULL DEFAULT "",
|
||||||
logo_opacity REAL NOT NULL DEFAULT 0.7,
|
logo_opacity REAL NOT NULL DEFAULT 0.7,
|
||||||
logo_position TEXT NOT NULL DEFAULT "W-w-12:12",
|
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_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 "",
|
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_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",
|
day_start TEXT NOT NULL DEFAULT "05:59:25",
|
||||||
length TEXT NOT NULL DEFAULT "24:00:00",
|
length TEXT NOT NULL DEFAULT "24:00:00",
|
||||||
infinit INTEGER NOT NULL DEFAULT 0,
|
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_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 "filler/filler.mp4",
|
||||||
filler TEXT NOT NULL DEFAULT "/var/lib/ffplayout/tv-media/filler/filler.mp4",
|
|
||||||
extensions TEXT NOT NULL DEFAULT "mp4;mkv;webm",
|
extensions TEXT NOT NULL DEFAULT "mp4;mkv;webm",
|
||||||
shuffle INTEGER NOT NULL DEFAULT 1,
|
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,
|
add_text INTEGER NOT NULL DEFAULT 1,
|
||||||
text_from_filename INTEGER NOT NULL DEFAULT 0,
|
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",
|
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)$",
|
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.",
|
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 "",
|
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_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_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
|
FOREIGN KEY (channel_id) REFERENCES channels (id) ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user