diff --git a/Cargo.lock b/Cargo.lock index 909a5d42..98daf9fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -549,9 +549,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -582,9 +582,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "awc" @@ -793,9 +793,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" dependencies = [ "clap_builder", "clap_derive", @@ -803,9 +803,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" dependencies = [ "anstream", "anstyle", @@ -815,9 +815,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", @@ -1215,7 +1215,7 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "ffplayout" -version = "0.24.0-rc1" +version = "0.24.0-beta5" dependencies = [ "actix-files", "actix-multipart", @@ -1314,9 +1314,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide", @@ -1697,9 +1697,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", "futures-channel", @@ -1710,7 +1710,6 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] @@ -2068,9 +2067,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.158" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libm" @@ -2520,26 +2519,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pin-project" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - [[package]] name = "pin-project-lite" version = "0.2.14" @@ -2575,9 +2554,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "powerfmt" @@ -2739,9 +2718,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" +checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" dependencies = [ "bitflags 2.6.0", ] @@ -3089,9 +3068,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -3681,7 +3660,7 @@ dependencies = [ [[package]] name = "tests" -version = "0.24.0-rc1" +version = "0.24.0-beta5" dependencies = [ "actix-rt", "actix-test", @@ -3711,18 +3690,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -3861,9 +3840,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.21" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap 2.5.0", "serde", @@ -3872,27 +3851,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - [[package]] name = "tower-service" version = "0.3.3" @@ -4495,9 +4453,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 723fff30..7299d4a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["engine", "tests"] resolver = "2" [workspace.package] -version = "0.24.0-rc1" +version = "0.24.0-beta5" license = "GPL-3.0" repository = "https://github.com/ffplayout/ffplayout" authors = ["Jonathan Baecker "] diff --git a/README.md b/README.md index b35be2c3..f88f70e5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) -## **ffplayout-engine (ffplayout)** +![player](/docs/images/player.png) [ffplayout](/ffplayout-engine/README.md) is a 24/7 broadcasting solution. It can playout a folder containing audio or video clips, or play a *JSON* playlist for each day, keeping the current playlist editable. diff --git a/docs/closed_captions.md b/docs/closed_captions.md new file mode 100644 index 00000000..c9cbf5e0 --- /dev/null +++ b/docs/closed_captions.md @@ -0,0 +1,23 @@ +## Closed Captions + +#### Note: +**This is only an _experimental feature_. Please be aware that bugs and unexpected behavior may occur. To utilize this feature, a [special patched](https://github.com/jb-alvarado/compile-ffmpeg-osx-linux) version of FFmpeg is required. Importantly, there is currently no official support for this functionality.** + +### Usage +**ffplayout** can handle closed captions in WebVTT format for HLS streaming. + +The captions can be embedded in the file, such as in a [Matroska](https://www.matroska.org/technical/subtitles.html) file, or they can be a separate *.vtt file that shares the same filename as the video file. In either case, the processing option **vtt_enable** must be enabled, and the path to the **vtt_dummy** file must exist. + +To encode the closed captions, the **hls** mode needs to be enabled, and specific output parameters must be provided. Here’s an example: + +``` +-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 \ +-muxpreload 0 -muxdelay 0 -f hls -hls_time 6 -hls_list_size 600 \ +-hls_flags append_list+delete_segments+omit_endlist \ +-var_stream_map v:0,a:0,s:0,sgroup:subs,name:English,language:en-US,default:YES \ +-master_pl_name master.m3u8 \ +-hls_segment_filename \ +live/stream-%d.ts live/stream.m3u8 +``` diff --git a/engine/src/db/handles.rs b/engine/src/db/handles.rs index c85cf642..5b1997c5 100644 --- a/engine/src/db/handles.rs +++ b/engine/src/db/handles.rs @@ -211,7 +211,7 @@ pub async fn update_configuration( id: i32, config: PlayoutConfig, ) -> Result { - let query = "UPDATE configurations SET general_stop_threshold = $2, mail_subject = $3, mail_smtp = $4, mail_addr = $5, mail_pass = $6, mail_recipient = $7, mail_starttls = $8, mail_level = $9, mail_interval = $10, logging_ffmpeg_level = $11, logging_ingest_level = $12, logging_detect_silence = $13, logging_ignore = $14, processing_mode = $15, processing_audio_only = $16, processing_copy_audio = $17, processing_copy_video = $18, processing_width = $19, processing_height = $20, processing_aspect = $21, processing_fps = $22, processing_add_logo = $23, processing_logo = $24, processing_logo_scale = $25, processing_logo_opacity = $26, processing_logo_position = $27, processing_audio_tracks = $28, processing_audio_track_index = $29, processing_audio_channels = $30, processing_volume = $31, processing_filter = $32, ingest_enable = $33, ingest_param = $34, ingest_filter = $35, playlist_day_start = $36, playlist_length = $37, playlist_infinit = $38, storage_filler = $39, storage_extensions = $40, storage_shuffle = $41, text_add = $42, text_from_filename = $43, text_font = $44, text_style = $45, text_regex = $46, task_enable = $47, task_path = $48, output_mode = $49, output_param = $50 WHERE id = $1"; + let query = "UPDATE configurations SET general_stop_threshold = $2, mail_subject = $3, mail_smtp = $4, mail_addr = $5, mail_pass = $6, mail_recipient = $7, mail_starttls = $8, mail_level = $9, mail_interval = $10, logging_ffmpeg_level = $11, logging_ingest_level = $12, logging_detect_silence = $13, logging_ignore = $14, processing_mode = $15, processing_audio_only = $16, processing_copy_audio = $17, processing_copy_video = $18, processing_width = $19, processing_height = $20, processing_aspect = $21, processing_fps = $22, processing_add_logo = $23, processing_logo = $24, processing_logo_scale = $25, processing_logo_opacity = $26, processing_logo_position = $27, processing_audio_tracks = $28, processing_audio_track_index = $29, processing_audio_channels = $30, processing_volume = $31, processing_filter = $32, processing_vtt_enable = $33, processing_vtt_dummy = $34, ingest_enable = $35, ingest_param = $36, ingest_filter = $37, playlist_day_start = $38, playlist_length = $39, playlist_infinit = $40, storage_filler = $41, storage_extensions = $42, storage_shuffle = $43, text_add = $44, text_from_filename = $45, text_font = $46, text_style = $47, text_regex = $48, task_enable = $49, task_path = $50, output_mode = $51, output_param = $52 WHERE id = $1"; sqlx::query(query) .bind(id) @@ -246,6 +246,8 @@ pub async fn update_configuration( .bind(config.processing.audio_channels) .bind(config.processing.volume) .bind(config.processing.custom_filter) + .bind(config.processing.vtt_enable) + .bind(config.processing.vtt_dummy) .bind(config.ingest.enable) .bind(config.ingest.input_param) .bind(config.ingest.custom_filter) diff --git a/engine/src/player/utils/mod.rs b/engine/src/player/utils/mod.rs index 648b006a..2dc0456c 100644 --- a/engine/src/player/utils/mod.rs +++ b/engine/src/player/utils/mod.rs @@ -625,6 +625,10 @@ pub fn loop_image(config: &PlayoutConfig, node: &Media) -> Vec { .storage_path .join(config.processing.vtt_dummy.clone().unwrap_or_default()); + if node.seek > 0.5 { + source_cmd.append(&mut vec_strings!["-ss", node.seek]); + } + if vtt_file.is_file() { source_cmd.append(&mut vec_strings![ "-i", @@ -732,6 +736,10 @@ pub fn seek_and_length(config: &PlayoutConfig, node: &mut Media) -> Vec .storage_path .join(config.processing.vtt_dummy.clone().unwrap_or_default()); + if node.seek > 0.5 { + source_cmd.append(&mut vec_strings!["-ss", node.seek]); + } + if vtt_file.is_file() { if loop_count > 1 { source_cmd.append(&mut vec_strings!["-stream_loop", loop_count]); diff --git a/engine/src/utils/args_parse.rs b/engine/src/utils/args_parse.rs index 62a9adde..6145b8ea 100644 --- a/engine/src/utils/args_parse.rs +++ b/engine/src/utils/args_parse.rs @@ -30,7 +30,12 @@ use crate::utils::db_path; #[derive(Parser, Debug, Clone)] #[clap(version, about = "ffplayout - 24/7 broadcasting solution", - long_about = None)] + long_about = Some("ffplayout - 24/7 broadcasting solution\n +Stream dynamic playlists or folder contents with the power of ffmpeg. +The target can be an HLS playlist, rtmp/srt/udp server, desktop player +or any other output supported by ffmpeg.\n +ffplayout also provides a web frontend and API to control streaming, +manage config, files, text overlay, etc. "))] pub struct Args { #[clap( short, @@ -43,20 +48,6 @@ pub struct Args { #[clap(short, long, help_heading = Some("Initial Setup"), help = "Add a global admin user")] pub add: bool, - #[clap(long, env, help_heading = Some("Initial Setup"), help = "Playlist root path")] - pub playlist_root: Option, - - #[clap(long, env, help_heading = Some("Initial Setup"), help = "Storage root path")] - pub storage_root: Option, - - #[clap( - long, - env, - help_heading = Some("Initial Setup"), - help = "Share storage root across channels, important for running in Container" - )] - pub shared_storage: bool, - #[clap(short, long, help_heading = Some("Initial Setup"), help = "Create admin user")] pub username: Option, @@ -66,18 +57,31 @@ pub struct Args { #[clap(short, long, help_heading = Some("Initial Setup"), help = "Admin password")] pub password: Option, - #[clap(long, env, help_heading = Some("Initial Setup"), help = "Logging path")] + #[clap(long, env, help_heading = Some("Initial Setup"), help = "Storage root path")] + pub storage: Option, + + #[clap( + long, + env, + help_heading = Some("Initial Setup"), + help = "Share storage across channels, important for running in Containers" + )] + pub shared_storage: bool, + + #[clap(long, env, help_heading = Some("Initial Setup / General"), help = "Logging path")] pub log_path: Option, - #[clap(long, env, help_heading = Some("Initial Setup"), help = "Path to public files, also HLS playlists")] + #[clap(long, env, help_heading = Some("Initial Setup / General"), help = "Path to public files, also HLS playlists")] pub public: Option, + #[clap(long, help_heading = Some("Initial Setup / Playlist"), help = "Path to playlist, or playlist root folder.")] + pub playlist: Option, + #[clap(long, env, help_heading = Some("General"), help = "Path to database file")] pub db: Option, #[clap( long, - env, help_heading = Some("General"), help = "Drop database. WARNING: this will delete all configurations!" )] @@ -110,6 +114,27 @@ pub struct Args { #[clap(short, env, long, help_heading = Some("General"), help = "Listen on IP:PORT, like: 127.0.0.1:8787")] pub listen: Option, + #[clap( + long, + env, + help_heading = Some("General"), + help = "Override logging level: trace, debug, println, warn, eprintln" + )] + pub log_level: Option, + + #[clap(long, env, help_heading = Some("General"), help = "Log to console")] + pub log_to_console: bool, + + #[clap( + short, + long, + env, + help_heading = Some("General / Playout"), + help = "Channels by ids to process (for export config, foreground running, etc.)", + num_args = 1.., + )] + pub channels: Option>, + #[clap( short, long, @@ -123,9 +148,6 @@ pub struct Args { #[clap(long, help_heading = Some("Playlist"), help = "Optional path list for playlist generations", num_args = 1..)] pub paths: Option>, - #[clap(long, help_heading = Some("Playlist"), help = "Path to playlist, or playlist root folder.")] - pub playlist: Option, - #[clap( short, long, @@ -134,22 +156,12 @@ pub struct Args { )] pub start: Option, - #[clap(short = 'T', long, help_heading = Some("Playlist"), help = "JSON Template file for generating playlist")] + #[clap(short = 'T', long, help_heading = Some("Playlist"), help = "JSON template file for generating playlist")] pub template: Option, #[clap(long, help_heading = Some("Playlist"), help = "Only validate given playlist")] pub validate: bool, - #[clap( - short, - long, - env, - help_heading = Some("Playout"), - help = "Channels by ids to process (for foreground, etc.)", - num_args = 1.., - )] - pub channels: Option>, - #[clap(long, env, help_heading = Some("Playout"), help = "Run playout without webserver and frontend.")] pub foreground: bool, @@ -159,17 +171,6 @@ pub struct Args { #[clap(long, env, help_heading = Some("Playout"), help = "Keep log file for given days")] pub log_backup_count: Option, - #[clap( - long, - env, - help_heading = Some("Playout"), - help = "Override logging level: trace, debug, println, warn, eprintln" - )] - pub log_level: Option, - - #[clap(long, env, help_heading = Some("Playout"), help = "Log to console")] - pub log_to_console: bool, - #[clap(short, long, help_heading = Some("Playout"), help = "Set output mode: desktop, hls, null, stream")] pub output: Option, @@ -393,8 +394,8 @@ pub async fn run_args(pool: &Pool) -> Result<(), i32> { } if !args.init - && args.storage_root.is_some() - && args.playlist_root.is_some() + && args.storage.is_some() + && args.playlist.is_some() && args.public.is_some() && args.log_path.is_some() { @@ -404,9 +405,9 @@ pub async fn run_args(pool: &Pool) -> Result<(), i32> { id: 0, secret: None, logging_path: args.log_path.unwrap().to_string_lossy().to_string(), - playlist_root: args.playlist_root.unwrap(), + playlist_root: args.playlist.unwrap(), public_root: args.public.unwrap(), - storage_root: args.storage_root.unwrap(), + storage_root: args.storage.unwrap(), shared_storage: args.shared_storage, }; diff --git a/engine/src/utils/config.rs b/engine/src/utils/config.rs index 3c63b775..df73bbaf 100644 --- a/engine/src/utils/config.rs +++ b/engine/src/utils/config.rs @@ -851,7 +851,7 @@ pub async fn get_config( } if let Some(playlist) = args.playlist { - config.channel.playlist_path = playlist; + config.channel.playlist_path = PathBuf::from(&playlist); } if let Some(folder) = args.folder { diff --git a/frontend/components/ConfigPlayout.vue b/frontend/components/ConfigPlayout.vue index 0bbe2a3a..dabd83a4 100644 --- a/frontend/components/ConfigPlayout.vue +++ b/frontend/components/ConfigPlayout.vue @@ -15,12 +15,10 @@ class="form-control w-full" :class="[typeof prop === 'boolean' && 'flex-row', name.toString() !== 'help_text' && 'mt-2']" > -