Merge pull request #189 from jb-alvarado/master

replace timezone with utc offset
This commit is contained in:
jb-alvarado 2022-09-06 14:31:06 +02:00 committed by GitHub
commit 62144886eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 112 additions and 25 deletions

2
Cargo.lock generated
View File

@ -960,7 +960,7 @@ dependencies = [
[[package]]
name = "ffplayout-api"
version = "0.5.4"
version = "0.6.0"
dependencies = [
"actix-files",
"actix-multipart",

View File

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

View File

@ -70,8 +70,8 @@ curl -X GET http://127.0.0.1:8787/api/channel/1 -H "Authorization: Bearer <TOKEN
"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"
}
```
@ -86,7 +86,7 @@ curl -X GET http://127.0.0.1:8787/api/channels -H "Authorization: Bearer <TOKEN>
```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 <TOKEN>"
```
@ -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 <TOKEN>"
```

View File

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

View File

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

View File

@ -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<SqliteQueryResult, sqlx::Error> {
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<String>) -> Result<&'static str, Box<dyn std
SELECT RAISE(FAIL, 'Database is already initialized!');
END;
INSERT INTO global(secret) VALUES($1);
INSERT INTO channels(name, preview_url, config_path, extra_extensions, timezone, service)
VALUES('Channel 1', $2, '/etc/ffplayout/ffplayout.yml', 'jpg,jpeg,png', 'UTC', 'ffplayout.service');
INSERT INTO channels(name, preview_url, config_path, extra_extensions, service)
VALUES('Channel 1', $2, '/etc/ffplayout/ffplayout.yml', 'jpg,jpeg,png', 'ffplayout.service');
INSERT INTO roles(name) VALUES('admin'), ('user'), ('guest');
INSERT INTO presets(name, text, x, y, fontsize, line_spacing, fontcolor, box, boxcolor, boxborderw, alpha, channel_id)
VALUES('Default', 'Wellcome to ffplayout messenger!', '(w-text_w)/2', '(h-text_h)/2', '24', '4', '#ffffff@0xff', '0', '#000000@0x80', '4', '1.0', '1'),
@ -150,19 +149,25 @@ pub async fn db_global() -> Result<GlobalSettings, sqlx::Error> {
pub async fn db_get_channel(id: &i64) -> Result<Channel, sqlx::Error> {
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<Vec<Channel>, sqlx::Error> {
let conn = db_connection().await?;
let query = "SELECT * FROM channels";
let result: Vec<Channel> = sqlx::query_as(query).fetch_all(&conn).await?;
let mut results: Vec<Channel> = 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<SqliteQueryResult, sqlx::Error> {
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<Channel, sqlx::Error> {
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?;

View File

@ -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<String, Servi
"Requested log file not exists, or not readable.".to_string(),
))
}
pub fn local_utc_offset() -> 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::<i32>().unwrap_or(0) * 60 + f.parse::<i32>().unwrap_or(0);
if operator == '-' && utc_offset > 0 {
utc_offset = -utc_offset;
}
}
utc_offset
}

View File

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

View File

@ -235,8 +235,8 @@ async fn add_user(data: web::Json<User>) -> Result<impl Responder, ServiceError>
/// "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<impl Responder, ServiceError> {
/// ```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 <TOKEN>"
/// ```
#[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 <TOKEN>"
/// ```
#[post("/channel/")]

@ -1 +1 @@
Subproject commit 2bc6ce9c32dd2c3bf44b6c174ad78318b26a6531
Subproject commit 37ccb38b91b34095d06673952a913be3cf79eb0a