diff --git a/Cargo.lock b/Cargo.lock index 41911677..f672e953 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -960,7 +960,7 @@ dependencies = [ [[package]] name = "ffplayout-api" -version = "0.5.4" +version = "0.6.0" dependencies = [ "actix-files", "actix-multipart", diff --git a/assets/ffplayout.yml b/assets/ffplayout.yml index d67e89f7..573ea462 100644 --- a/assets/ffplayout.yml +++ b/assets/ffplayout.yml @@ -120,7 +120,7 @@ out: 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. In production don't serve hls playlist with ffpapi, use nginx or another web server! - mode: hls + mode: desktop output_param: >- -c:v libx264 -crf 23 diff --git a/docs/api.md b/docs/api.md index 020ddcaf..d2b21627 100644 --- a/docs/api.md +++ b/docs/api.md @@ -70,8 +70,8 @@ curl -X GET http://127.0.0.1:8787/api/channel/1 -H "Authorization: Bearer ```BASH curl -X PATCH http://127.0.0.1:8787/api/channel/1 -H "Content-Type: application/json" \ -d '{ "id": 1, "name": "Channel 1", "preview_url": "http://localhost/live/stream.m3u8", \ -"config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png", "timezone": "Europe/Berlin"}' \ +"config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png"}' \ -H "Authorization: Bearer " ``` @@ -96,7 +96,7 @@ curl -X PATCH http://127.0.0.1:8787/api/channel/1 -H "Content-Type: application/ curl -X POST http://127.0.0.1:8787/api/channel/ -H "Content-Type: application/json" \ -d '{ "name": "Channel 2", "preview_url": "http://localhost/live/channel2.m3u8", \ "config_path": "/etc/ffplayout/channel2.yml", "extra_extensions": "jpg,jpeg,png", -"timezone": "Europe/Berlin", "service": "ffplayout@channel2.service" }' \ +"service": "ffplayout@channel2.service" }' \ -H "Authorization: Bearer " ``` diff --git a/docs/custom_filters.md b/docs/custom_filters.md index 6a8c9b10..ef3a8717 100644 --- a/docs/custom_filters.md +++ b/docs/custom_filters.md @@ -26,5 +26,69 @@ Pay attention to the filter prefix `[v_in];`, this is necessary to get the outpu custom_filter: edgedetect=mode=colormix:high=0[c_v_out] ``` -Check ffmpeg [filters](https://ffmpeg.org/ffmpeg-filters.html) documentation, and find out which other filters ffmpeg has. +### Where the filters applied in stream mode +The **custom filter** from **config -> processing** and from **playlist** got applied in the _decoder_ instance on every file: + +``` + +-------------------------------------------------- + + | file loop | + | +-------------------------------------+ | PIPE +------------------------+ + | input -> | decoder / filtering / custom filter |-------------| encoder / text overlay | -> output + | +-------------------------------------+ | +------------------------+ + | start new on file change | constant output + +---------------------------------------------------+ +``` + +#### When take which + +* If you want to use for every clip a different filter chain, you should use the custom filter parameter from **playlist**. +* When you want to use the same filter for every clip you can use the custom filter from **config -> processing**. + +### Complex example + +This example takes a image and a animated mov clip with alpha and overlays them two times on different positions in time: + +```YAML +custom_filter: '[v_in];movie=image_input.png:s=v,loop=loop=250.0:size=1:start=0,scale=1024:576,split=2[lower_1_out_1][lower_1_out_2];[lower_1_out_1]fifo,fade=in:duration=0.5:alpha=1,fade=out:start_time=9.5:duration=0.5:alpha=1,setpts=PTS+5.0/TB[fade_1];[v_in][fade_1]overlay=enable=between(t\,5.0\,15.0)[base_1];[lower_1_out_2]fifo,fade=in:duration=0.5:alpha=1,fade=out:start_time=9.5:duration=0.5:alpha=1,setpts=PTS+30.0/TB[fade_2];[base_1][fade_2]overlay=enable=between(t\,30.0\,40.0)[base_2];movie=animated_input.mov:s=v,scale=1024:576,split=2[lower_2_out_1][lower_2_out_2];[lower_2_out_1]fifo,setpts=PTS+20.0/TB[layer_1];[base_2][layer_1]overlay=repeatlast=0[base_3];[lower_2_out_2]fifo,setpts=PTS+50.0/TB[layer_2];[base_3][layer_2]overlay=repeatlast=0[c_v_out]' +``` + +And here are the explanation for each filter: + +```PYTHON + +# get input from video +[v_in]; + +# load the image, loops it for 10 seconds (25 FPS * 10), scale it to the target resolution, splits it into two outputs +movie=image_input.png:s=v,loop=loop=250.0:size=1:start=0,scale=1024:576,split=2[lower_1_out_1][lower_1_out_2]; + +# take output one from image, fades it in for 0.5 seconds, fades it out for 0.5 seconds, shift the start time to 00:00:05 (5 seconds) +[lower_1_out_1]fifo,fade=in:duration=0.5:alpha=1,fade=out:start_time=9.5:duration=0.5:alpha=1,setpts=PTS+5.0/TB[fade_1]; + +# overlay first output on top of the video, between second 5 and 15 +[v_in][fade_1]overlay=enable=between(t\,5.0\,15.0)[base_1]; + +# take output two from image, fades it in for 0.5 seconds, fades it out for 0.5 seconds, shift the start time to 00:00:30 (30 seconds) +[lower_1_out_2]fifo,fade=in:duration=0.5:alpha=1,fade=out:start_time=9.5:duration=0.5:alpha=1,setpts=PTS+30.0/TB[fade_2]; + +# overlay second output on top of output from last overlay, between second 30 and 40 +[base_1][fade_2]overlay=enable=between(t\,30.0\,40.0)[base_2]; + +# load the animated clip with alpha, scale it to the target resolution, splits it into two outputs +movie=animated_input.mov:s=v,scale=1024:576,split=2[lower_2_out_1][lower_2_out_2]; + +# shift the start from first animated clip to second 20 +[lower_2_out_1]fifo,setpts=PTS+20.0/TB[layer_1]; + +# overlay the shifted animation on top of the last image overlay +[base_2][layer_1]overlay=repeatlast=0[base_3]; + +# shift the start from second animated clip to second 50 +[lower_2_out_2]fifo,setpts=PTS+50.0/TB[layer_2]; + +# overlay the second shifted animation on top of the last overlay +[base_3][layer_2]overlay=repeatlast=0[c_v_out] +``` + +Check ffmpeg [filters](https://ffmpeg.org/ffmpeg-filters.html) documentation, and find out which other filters ffmpeg has and how to apply. diff --git a/ffplayout-api/Cargo.toml b/ffplayout-api/Cargo.toml index a3cd863c..b89eb28e 100644 --- a/ffplayout-api/Cargo.toml +++ b/ffplayout-api/Cargo.toml @@ -4,7 +4,7 @@ description = "Rest API for ffplayout" license = "GPL-3.0" authors = ["Jonathan Baecker jonbae77@gmail.com"] readme = "README.md" -version = "0.5.4" +version = "0.6.0" edition = "2021" [dependencies] diff --git a/ffplayout-api/src/utils/handles.rs b/ffplayout-api/src/utils/handles.rs index 6ab132e0..f505b5de 100644 --- a/ffplayout-api/src/utils/handles.rs +++ b/ffplayout-api/src/utils/handles.rs @@ -8,7 +8,7 @@ use simplelog::*; use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, SqlitePool}; use crate::utils::{ - db_path, + db_path, local_utc_offset, models::{Channel, TextPreset, User}, GlobalSettings, }; @@ -40,7 +40,6 @@ async fn create_schema() -> Result { preview_url TEXT NOT NULL, config_path TEXT NOT NULL, extra_extensions TEXT NOT NULL, - timezone TEXT NOT NULL, service TEXT NOT NULL, UNIQUE(name, service) ); @@ -111,8 +110,8 @@ pub async fn db_init(domain: Option) -> Result<&'static str, Box Result { pub async fn db_get_channel(id: &i64) -> Result { let conn = db_connection().await?; let query = "SELECT * FROM channels WHERE id = $1"; - let result: Channel = sqlx::query_as(query).bind(id).fetch_one(&conn).await?; + let mut result: Channel = sqlx::query_as(query).bind(id).fetch_one(&conn).await?; conn.close().await; + result.utc_offset = local_utc_offset(); + Ok(result) } pub async fn db_get_all_channels() -> Result, sqlx::Error> { let conn = db_connection().await?; let query = "SELECT * FROM channels"; - let result: Vec = sqlx::query_as(query).fetch_all(&conn).await?; + let mut results: Vec = sqlx::query_as(query).fetch_all(&conn).await?; conn.close().await; - Ok(result) + for result in results.iter_mut() { + result.utc_offset = local_utc_offset(); + } + + Ok(results) } pub async fn db_update_channel( @@ -171,14 +176,13 @@ pub async fn db_update_channel( ) -> Result { let conn = db_connection().await?; - let query = "UPDATE channels SET name = $2, preview_url = $3, config_path = $4, extra_extensions = $5, timezone = $6 WHERE id = $1"; + let query = "UPDATE channels SET name = $2, preview_url = $3, config_path = $4, extra_extensions = $5 WHERE id = $1"; let result: SqliteQueryResult = sqlx::query(query) .bind(id) .bind(channel.name) .bind(channel.preview_url) .bind(channel.config_path) .bind(channel.extra_extensions) - .bind(channel.timezone) .execute(&conn) .await?; conn.close().await; @@ -189,13 +193,12 @@ pub async fn db_update_channel( pub async fn db_add_channel(channel: Channel) -> Result { let conn = db_connection().await?; - let query = "INSERT INTO channels (name, preview_url, config_path, extra_extensions, timezone, service) VALUES($1, $2, $3, $4, $5, $6)"; + let query = "INSERT INTO channels (name, preview_url, config_path, extra_extensions, service) VALUES($1, $2, $3, $4, $5)"; let result = sqlx::query(query) .bind(channel.name) .bind(channel.preview_url) .bind(channel.config_path) .bind(channel.extra_extensions) - .bind(channel.timezone) .bind(channel.service) .execute(&conn) .await?; diff --git a/ffplayout-api/src/utils/mod.rs b/ffplayout-api/src/utils/mod.rs index 00386ca3..e60ed063 100644 --- a/ffplayout-api/src/utils/mod.rs +++ b/ffplayout-api/src/utils/mod.rs @@ -5,6 +5,7 @@ use std::{ path::Path, }; +use chrono::prelude::*; use faccess::PathExt; use once_cell::sync::OnceCell; use rpassword::read_password; @@ -222,3 +223,19 @@ pub async fn read_log_file(channel_id: &i64, date: &str) -> Result i32 { + let mut offset = Local::now().format("%:z").to_string(); + let operator = offset.remove(0); + let mut utc_offset = 0; + + if let Some((r, f)) = offset.split_once(':') { + utc_offset = r.parse::().unwrap_or(0) * 60 + f.parse::().unwrap_or(0); + + if operator == '-' && utc_offset > 0 { + utc_offset = -utc_offset; + } + } + + utc_offset +} diff --git a/ffplayout-api/src/utils/models.rs b/ffplayout-api/src/utils/models.rs index 0e8fdd1d..df57b73e 100644 --- a/ffplayout-api/src/utils/models.rs +++ b/ffplayout-api/src/utils/models.rs @@ -66,6 +66,9 @@ pub struct Channel { pub preview_url: String, pub config_path: String, pub extra_extensions: String, - pub timezone: String, pub service: String, + + #[sqlx(default)] + #[serde(default)] + pub utc_offset: i32, } diff --git a/ffplayout-api/src/utils/routes.rs b/ffplayout-api/src/utils/routes.rs index 28a41b09..da750d4c 100644 --- a/ffplayout-api/src/utils/routes.rs +++ b/ffplayout-api/src/utils/routes.rs @@ -235,8 +235,8 @@ async fn add_user(data: web::Json) -> Result /// "preview_url": "http://localhost/live/preview.m3u8", /// "config_path": "/etc/ffplayout/ffplayout.yml", /// "extra_extensions": "jpg,jpeg,png", -/// "timezone": "UTC", -/// "service": "ffplayout.service" +/// "service": "ffplayout.service", +/// "utc_offset": "+120" /// } /// ``` #[get("/channel/{id}")] @@ -269,7 +269,7 @@ async fn get_all_channels() -> Result { /// ```BASH /// curl -X PATCH http://127.0.0.1:8787/api/channel/1 -H "Content-Type: application/json" \ /// -d '{ "id": 1, "name": "Channel 1", "preview_url": "http://localhost/live/stream.m3u8", \ -/// "config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png", "timezone": "Europe/Berlin"}' \ +/// "config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png"}' \ /// -H "Authorization: Bearer " /// ``` #[patch("/channel/{id}")] @@ -291,7 +291,7 @@ async fn patch_channel( /// curl -X POST http://127.0.0.1:8787/api/channel/ -H "Content-Type: application/json" \ /// -d '{ "name": "Channel 2", "preview_url": "http://localhost/live/channel2.m3u8", \ /// "config_path": "/etc/ffplayout/channel2.yml", "extra_extensions": "jpg,jpeg,png", -/// "timezone": "Europe/Berlin", "service": "ffplayout@channel2.service" }' \ +/// "service": "ffplayout@channel2.service" }' \ /// -H "Authorization: Bearer " /// ``` #[post("/channel/")] diff --git a/ffplayout-frontend b/ffplayout-frontend index 2bc6ce9c..37ccb38b 160000 --- a/ffplayout-frontend +++ b/ffplayout-frontend @@ -1 +1 @@ -Subproject commit 2bc6ce9c32dd2c3bf44b6c174ad78318b26a6531 +Subproject commit 37ccb38b91b34095d06673952a913be3cf79eb0a