diff --git a/Cargo.lock b/Cargo.lock index 8d21a294..d87820ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2924,9 +2924,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.35" +version = "0.38.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" +checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36" dependencies = [ "bitflags 2.6.0", "errno", @@ -3842,9 +3842,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ "futures-core", "pin-project-lite", diff --git a/docker/Dockerfile b/docker/Dockerfile index 81f356bb..8a9e1af9 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,6 @@ FROM alpine:latest -ARG FFPLAYOUT_VERSION=0.24.0-beta2 +ARG FFPLAYOUT_VERSION=0.24.0-beta4 ARG SHARED_STORAGE=false ENV DB=/db @@ -12,7 +12,7 @@ COPY <<-EOT /run.sh #!/bin/sh if [ ! -f /db/ffplayout.db ]; then - ffplayout -u admin -p admin -m contact@example.com --storage-root "/tv-media" --playlist-root "/playlists" --public-root "/public" --log-path "/logging" --shared-storage + ffplayout -u admin -p admin -m contact@example.com --storage-root "/tv-media" --playlist-root "/playlists" --public "/public" --log-path "/logging" --shared-storage fi /usr/bin/ffplayout -l "0.0.0.0:8787" diff --git a/docker/nonfree.Dockerfile b/docker/nonfree.Dockerfile index 88c955d1..bd6f27bb 100644 --- a/docker/nonfree.Dockerfile +++ b/docker/nonfree.Dockerfile @@ -1,6 +1,6 @@ FROM alpine:latest -ARG FFPLAYOUT_VERSION=0.24.0-beta2 +ARG FFPLAYOUT_VERSION=0.24.0-beta4 ARG SHARED_STORAGE=false ENV DB=/db @@ -14,7 +14,7 @@ COPY <<-EOT /run.sh #!/bin/sh if [ ! -f /db/ffplayout.db ]; then - ffplayout -u admin -p admin -m contact@example.com --storage-root "/tv-media" --playlist-root "/playlists" --public-root "/public" --log-path "/logging" --shared-storage + ffplayout -u admin -p admin -m contact@example.com --storage-root "/tv-media" --playlist-root "/playlists" --public "/public" --log-path "/logging" --shared-storage fi /usr/bin/ffplayout -l "0.0.0.0:8787" diff --git a/docker/nvidia.Dockerfile b/docker/nvidia.Dockerfile index 10d668b7..285ba915 100644 --- a/docker/nvidia.Dockerfile +++ b/docker/nvidia.Dockerfile @@ -1,6 +1,6 @@ FROM nvidia/cuda:12.5.0-runtime-rockylinux9 -ARG FFPLAYOUT_VERSION=0.24.0-beta2 +ARG FFPLAYOUT_VERSION=0.24.0-beta4 ARG SHARED_STORAGE=false ENV DB=/db @@ -204,7 +204,7 @@ COPY <<-EOT /run.sh #!/bin/sh if [ ! -f /db/ffplayout.db ]; then - ffplayout -u admin -p admin -m contact@example.com --storage-root "/tv-media" --playlist-root "/playlists" --public-root "/public" --log-path "/logging" --shared-storage + ffplayout -u admin -p admin -m contact@example.com --storage-root "/tv-media" --playlist-root "/playlists" --public "/public" --log-path "/logging" --shared-storage fi /usr/bin/ffplayout -l "0.0.0.0:8787" diff --git a/ffplayout/src/api/routes.rs b/ffplayout/src/api/routes.rs index 2d3c85bb..7c7b4f8e 100644 --- a/ffplayout/src/api/routes.rs +++ b/ffplayout/src/api/routes.rs @@ -52,10 +52,10 @@ use crate::utils::{ playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist}, public_path, read_log_file, system, TextFilter, }; -use crate::vec_strings; use crate::{ api::auth::{create_jwt, Claims}, utils::advanced_config::AdvancedConfig, + vec_strings, }; use crate::{ db::{ @@ -1309,24 +1309,21 @@ async fn get_file( /// Can be used for HLS Playlist and other static files in public folder /// /// ```BASH -/// curl -X GET http://127.0.0.1:8787/live/1/stream.m3u8 +/// curl -X GET http://127.0.0.1:8787/1/live/stream.m3u8 /// ``` -#[get("/{public:live|preview|public}/{id}/{file_stem:.*}")] +#[get("/{id}/{public:live|preview|public}/{file_stem:.*}")] async fn get_public( - path: web::Path<(String, i32, String)>, + path: web::Path<(i32, String, String)>, controllers: web::Data>, ) -> Result { - let (public, id, file_stem) = path.into_inner(); - let public_path = public_path(); + let (id, public, file_stem) = path.into_inner(); let absolute_path = if file_stem.ends_with(".ts") || file_stem.ends_with(".m3u8") { let manager = controllers.lock().unwrap().get(id).unwrap(); let config = manager.config.lock().unwrap(); config.channel.hls_path.join(public) - } else if public_path.is_absolute() { - public_path.to_path_buf() } else { - env::current_dir()?.join(public_path) + public_path() } .clean(); diff --git a/ffplayout/src/main.rs b/ffplayout/src/main.rs index 5897fc57..32776087 100644 --- a/ffplayout/src/main.rs +++ b/ffplayout/src/main.rs @@ -1,6 +1,5 @@ use std::{ collections::HashSet, - env, fs::File, io, process::exit, @@ -8,16 +7,16 @@ use std::{ thread, }; -use actix_files::Files; use actix_web::{middleware::Logger, web, App, HttpServer}; - use actix_web_httpauth::middleware::HttpAuthentication; +#[cfg(any(debug_assertions, not(feature = "embed_frontend")))] +use actix_files::Files; + #[cfg(all(not(debug_assertions), feature = "embed_frontend"))] use actix_web_static_files::ResourceFiles; use log::*; -use path_clean::PathClean; use ffplayout::{ api::routes::*, @@ -105,12 +104,14 @@ async fn main() -> std::io::Result<()> { info!("Running ffplayout API, listen on http://{conn}"); + let db_clone = pool.clone(); + // no 'allow origin' here, give it to the reverse proxy HttpServer::new(move || { let queues = mail_queues.clone(); let auth = HttpAuthentication::bearer(validator); - let db_pool = web::Data::new(pool.clone()); + let db_pool = web::Data::new(db_clone.clone()); // Customize logging format to get IP though proxies. let logger = Logger::new("%{r}a \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T") .exclude_regex(r"/_nuxt/*"); @@ -171,18 +172,7 @@ async fn main() -> std::io::Result<()> { ) .service(get_file); - if let Some(public) = &ARGS.public { - // When public path is set as argument use this path for serving extra static files, - // is useful for HLS stream etc. - let absolute_path = if public.is_absolute() { - public.to_path_buf() - } else { - env::current_dir().unwrap_or_default().join(public) - } - .clean(); - - web_app = web_app.service(Files::new("/", absolute_path)); - } else { + if ARGS.public.is_none() { // When no public path is given as argument, use predefine keywords in path, // like /live; /preview; /public, or HLS extensions to recognize file should get from public folder web_app = web_app.service(get_public); @@ -283,5 +273,7 @@ async fn main() -> std::io::Result<()> { channel_ctl.stop_all(); } + pool.close().await; + Ok(()) } diff --git a/ffplayout/src/player/controller.rs b/ffplayout/src/player/controller.rs index 1c463ee1..3faa1667 100644 --- a/ffplayout/src/player/controller.rs +++ b/ffplayout/src/player/controller.rs @@ -264,7 +264,7 @@ impl ChannelManager { self.run_count.fetch_sub(1, Ordering::SeqCst); let pool = self.db_pool.clone().unwrap(); let channel_id = self.channel.lock().unwrap().id; - debug!(target: Target::all(), channel = channel_id; "Stop all child processes from channel {channel_id}"); + debug!(target: Target::all(), channel = channel_id; "Deactivate playout and stop all child processes from channel: {channel_id}"); if let Err(e) = handles::update_player(&pool, channel_id, false).await { error!(target: Target::all(), channel = channel_id; "Unable write to player status: {e}"); @@ -273,7 +273,7 @@ impl ChannelManager { for unit in [Decoder, Encoder, Ingest] { if let Err(e) = self.stop(unit) { if !e.to_string().contains("exited process") { - error!("{e}") + error!(target: Target::all(), channel = channel_id; "{e}") } } } @@ -286,7 +286,7 @@ impl ChannelManager { self.ingest_is_running.store(false, Ordering::SeqCst); self.run_count.fetch_sub(1, Ordering::SeqCst); let channel_id = self.channel.lock().unwrap().id; - debug!(target: Target::all(), channel = channel_id; "Stop all child processes from channel {channel_id}"); + debug!(target: Target::all(), channel = channel_id; "Stop all child processes from channel: {channel_id}"); for unit in [Decoder, Encoder, Ingest] { if let Err(e) = self.stop(unit) { diff --git a/ffplayout/src/player/input/playlist.rs b/ffplayout/src/player/input/playlist.rs index c3098314..c87fd029 100644 --- a/ffplayout/src/player/input/playlist.rs +++ b/ffplayout/src/player/input/playlist.rs @@ -356,12 +356,23 @@ impl CurrentProgram { } fn recalculate_begin(&mut self, extend: bool) { - debug!(target: Target::file_mail(), channel = self.id; "Infinit playlist reaches end, recalculate clip begins."); + debug!(target: Target::file_mail(), channel = self.id; "Infinit playlist reaches end, recalculate clip begins. Extend: {extend}"); let mut time_sec = time_in_seconds(); if extend { - time_sec = self.start_sec + self.json_playlist.length.unwrap(); + // Calculate the elapsed time since the playlist start + let elapsed_sec = if time_sec >= self.start_sec { + time_sec - self.start_sec + } else { + time_sec + 86400.0 - self.start_sec + }; + + // Time passed within the current playlist loop + let time_in_current_loop = elapsed_sec % self.json_playlist.length.unwrap(); + + // Adjust the start time so that the playlist starts at the correct point in time + time_sec -= time_in_current_loop; } self.json_playlist.start_sec = Some(time_sec); diff --git a/ffplayout/src/utils/args_parse.rs b/ffplayout/src/utils/args_parse.rs index 7b9fc043..1707be3b 100644 --- a/ffplayout/src/utils/args_parse.rs +++ b/ffplayout/src/utils/args_parse.rs @@ -86,9 +86,6 @@ pub struct Args { #[clap(long, help = "List available channel ids")] pub list_channels: bool, - #[clap(long, env, help = "path to public files")] - pub public: Option, - #[clap(short, env, long, help = "Listen on IP:PORT, like: 127.0.0.1:8787")] pub listen: Option, @@ -123,8 +120,8 @@ pub struct Args { #[clap(long, env, help = "Log to console")] pub log_to_console: bool, - #[clap(long, env, help = "Public (HLS) output path")] - pub public_root: Option, + #[clap(long, env, help = "Path to public files, also HLS playlists")] + pub public: Option, #[clap(long, env, help = "Playlist root path")] pub playlist_root: Option, @@ -438,7 +435,7 @@ pub async fn run_args(pool: &Pool) -> Result<(), i32> { if !args.init && args.storage_root.is_some() && args.playlist_root.is_some() - && args.public_root.is_some() + && args.public.is_some() && args.log_path.is_some() { error_code = 0; @@ -448,7 +445,7 @@ pub async fn run_args(pool: &Pool) -> Result<(), i32> { secret: None, logging_path: args.log_path.unwrap().to_string_lossy().to_string(), playlist_root: args.playlist_root.unwrap(), - public_root: args.public_root.unwrap(), + public_root: args.public.unwrap(), storage_root: args.storage_root.unwrap(), shared_storage: args.shared_storage, }; @@ -523,19 +520,6 @@ pub async fn run_args(pool: &Pool) -> Result<(), i32> { }; } - if let Some(id) = ARGS.dump_config { - match PlayoutConfig::dump(pool, id).await { - Ok(_) => { - println!("Dump config to: ffplayout_{id}.toml"); - error_code = 0; - } - Err(e) => { - eprintln!("Dump config: {e}"); - error_code = 1; - } - }; - } - if let Some(id) = ARGS.dump_advanced { match AdvancedConfig::dump(pool, id).await { Ok(_) => { diff --git a/ffplayout/src/utils/config.rs b/ffplayout/src/utils/config.rs index 9172bf0e..9800fddc 100644 --- a/ffplayout/src/utils/config.rs +++ b/ffplayout/src/utils/config.rs @@ -407,14 +407,14 @@ impl Playlist { #[derive(Debug, Default, Clone, Deserialize, Serialize)] pub struct Storage { pub help_text: String, - #[serde(skip_deserializing)] + #[serde(skip_serializing, skip_deserializing)] pub path: PathBuf, #[serde(skip_serializing, skip_deserializing)] pub paths: Vec, pub filler: PathBuf, pub extensions: Vec, pub shuffle: bool, - #[serde(skip_deserializing)] + #[serde(skip_serializing, skip_deserializing)] pub shared_storage: bool, } @@ -603,10 +603,14 @@ impl PlayoutConfig { playlist.length_sec = Some(86400.0); } - if processing.add_logo && !Path::new(&processing.logo).is_file() { + let (logo_path, _, _) = norm_abs_path(&channel.storage_path, &processing.logo)?; + + if processing.add_logo && !logo_path.is_file() { processing.add_logo = false; } + processing.logo = logo_path.to_string_lossy().to_string(); + if processing.audio_tracks < 1 { processing.audio_tracks = 1 } @@ -711,6 +715,9 @@ impl PlayoutConfig { text.node_pos = None; } + let (text_path, _, _) = norm_abs_path(&channel.storage_path, &text.fontfile)?; + text.fontfile = text_path.to_string_lossy().to_string(); + Ok(Self { channel, advanced, @@ -859,6 +866,12 @@ pub async fn get_config( } } + if args.shared_storage { + // config.channel.shared_storage could be true already, + // so should not be overridden with false when args.shared_storage is not set + config.channel.shared_storage = args.shared_storage + } + if let Some(volume) = args.volume { config.processing.volume = volume; } diff --git a/ffplayout/src/utils/logging.rs b/ffplayout/src/utils/logging.rs index f58d3b7d..1c6d6d5d 100644 --- a/ffplayout/src/utils/logging.rs +++ b/ffplayout/src/utils/logging.rs @@ -305,6 +305,10 @@ pub fn log_file_path() -> PathBuf { .clone() .unwrap_or(PathBuf::from(&config.logging_path)); + if !log_path.is_absolute() { + log_path = env::current_dir().unwrap().join(log_path); + } + if !log_path.is_dir() { log_path = env::current_dir().unwrap(); } diff --git a/ffplayout/src/utils/mod.rs b/ffplayout/src/utils/mod.rs index a5fcd26b..3c6d3d69 100644 --- a/ffplayout/src/utils/mod.rs +++ b/ffplayout/src/utils/mod.rs @@ -30,6 +30,7 @@ pub mod playlist; pub mod system; pub mod task_runner; +use crate::db::models::GlobalSettings; use crate::player::utils::time_to_sec; use crate::utils::{errors::ServiceError, logging::log_file_path}; use crate::ARGS; @@ -195,19 +196,28 @@ pub fn db_path() -> Result<&'static str, Box> { } pub fn public_path() -> PathBuf { - let path = PathBuf::from("./ffplayout-frontend/.output/public/"); + let config = GlobalSettings::global(); + let dev_path = env::current_dir() + .unwrap_or_default() + .join("frontend/.output/public/"); + let mut public_path = PathBuf::from(&config.public_root); - if cfg!(debug_assertions) && path.is_dir() { - return path; + if let Some(p) = &ARGS.public { + // When public path is set as argument use this path for serving static files. + // Works only when feature embed_frontend is not set. + let public = PathBuf::from(p); + + public_path = if public.is_absolute() { + public.to_path_buf() + } else { + env::current_dir().unwrap_or_default().join(public) + } + .clean(); + } else if cfg!(debug_assertions) && dev_path.is_dir() { + public_path = dev_path; } - let path = PathBuf::from("/usr/share/ffplayout/public/"); - - if path.is_dir() { - return path; - } - - PathBuf::from("./public/") + public_path } pub async fn read_log_file(channel_id: &i32, date: &str) -> Result { diff --git a/frontend b/frontend index 48f123bf..bb744685 160000 --- a/frontend +++ b/frontend @@ -1 +1 @@ -Subproject commit 48f123bf6ad136968495e9e5e22249b8ca5ef192 +Subproject commit bb7446850c683c3a4465c336e348476d3c8bb49c diff --git a/migrations/00001_create_tables.sql b/migrations/00001_create_tables.sql index 72f64562..4fe57f9b 100644 --- a/migrations/00001_create_tables.sql +++ b/migrations/00001_create_tables.sql @@ -188,7 +188,7 @@ INSERT INTO VALUES ( 'Channel 1', - 'http://127.0.0.1:8787/live/1/stream.m3u8', + 'http://127.0.0.1:8787/1/live/stream.m3u8', 'jpg,jpeg,png', 0 );