diff --git a/Cargo.lock b/Cargo.lock index 4f587ea..18b13c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,6 +335,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -420,12 +426,58 @@ version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" +dependencies = [ + "arrayvec", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -478,6 +530,12 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + [[package]] name = "block-buffer" version = "0.10.4" @@ -518,6 +576,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "built" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b" + [[package]] name = "bumpalo" version = "3.16.0" @@ -536,6 +600,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.8.0" @@ -562,6 +632,16 @@ dependencies = [ "shlex", ] +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -592,12 +672,6 @@ dependencies = [ "inout", ] -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "colorchoice" version = "1.0.3" @@ -798,7 +872,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -1102,15 +1175,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - [[package]] name = "http" version = "0.2.12" @@ -1316,16 +1380,18 @@ dependencies = [ [[package]] name = "image" -version = "0.24.9" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", - "byteorder", - "color_quant", - "jpeg-decoder", + "byteorder-lite", "num-traits", "png", + "ravif", + "rayon", + "zune-core", + "zune-jpeg", ] [[package]] @@ -1346,7 +1412,6 @@ dependencies = [ "dotenv", "env_logger", "futures", - "hmac", "image", "jsonwebtoken", "lazy_static", @@ -1358,10 +1423,16 @@ dependencies = [ "rayon", "serde", "serde_json", - "sha2", + "tokio", "walkdir", ] +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + [[package]] name = "impl-more" version = "0.1.8" @@ -1407,12 +1478,32 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.13" @@ -1428,15 +1519,6 @@ dependencies = [ "libc", ] -[[package]] -name = "jpeg-decoder" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" -dependencies = [ - "rayon", -] - [[package]] name = "js-sys" version = "0.3.72" @@ -1499,6 +1581,16 @@ version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +[[package]] +name = "libfuzzer-sys" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libredox" version = "0.1.3" @@ -1565,6 +1657,25 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1608,6 +1719,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -1643,6 +1760,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "notify" version = "6.1.1" @@ -1678,6 +1817,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -1687,6 +1837,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1835,6 +1996,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "prometheus" version = "0.13.4" @@ -1856,6 +2036,12 @@ version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.37" @@ -1895,6 +2081,56 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "rayon" version = "1.10.0" @@ -1959,6 +2195,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" + [[package]] name = "ring" version = "0.17.8" @@ -2102,17 +2344,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "1.3.0" @@ -2134,6 +2365,15 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simple_asn1" version = "0.6.2" @@ -2223,6 +2463,25 @@ dependencies = [ "syn", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.14.0" @@ -2299,9 +2558,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.41.1" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", @@ -2434,6 +2693,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "v_htmlescape" version = "0.15.8" @@ -2446,6 +2716,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.5" @@ -2831,3 +3107,18 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 39e32af..da54841 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ lto = true actix = "0.13.1" actix-web = "4" actix-rt = "2.6" +tokio = { version = "1.42.0", features = ["default", "process", "sync"] } actix-files = "0.6" actix-multipart = "0.7.2" futures = "0.3.5" @@ -20,19 +21,17 @@ jsonwebtoken = "9.3.0" serde = "1" serde_json = "1" diesel = { version = "2.2.5", features = ["sqlite"] } -diesel_migrations = "2.0.0" -hmac = "0.12.1" -sha2 = "0.10.8" +diesel_migrations = "2.2.0" chrono = "0.4" dotenv = "0.15" bcrypt = "0.16.0" -image = { version = "0.24.9", default-features = false, features = ["jpeg", "png", "jpeg_rayon"] } +image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "rayon"] } walkdir = "2.4.0" rayon = "1.5" notify = "6.1.1" path-absolutize = "3.1" -log="0.4" -env_logger= "0.11.5" +log = "0.4" +env_logger = "0.11.5" actix-web-prom = "0.9.0" prometheus = "0.13" lazy_static = "1.5" diff --git a/README.md b/README.md index 3c5b5af..0022e18 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ You must have `ffmpeg` installed for streaming video and generating video thumbn - `DATABASE_URL` is a path or url to a database (currently only SQLite is tested) - `BASE_PATH` is the root from which you want to serve images and videos - `THUMBNAILS` is a path where generated thumbnails should be stored +- `VIDEO_PATH` is a path where HLS playlists and video parts should be stored - `BIND_URL` is the url and port to bind to (typically your own IP address) - `SECRET_KEY` is the *hopefully* random string to sign Tokens with - `RUST_LOG` is one of `off, error, warn, info, debug, trace`, from least to most noisy [error is default] diff --git a/src/main.rs b/src/main.rs index 6f8d7de..b46338d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -212,9 +212,9 @@ async fn generate_video( ) -> impl Responder { let filename = PathBuf::from(&body.path); - if let Some(name) = filename.file_stem() { + if let Some(name) = filename.file_name() { let filename = name.to_str().expect("Filename should convert to string"); - let playlist = format!("tmp/{}.m3u8", filename); + let playlist = format!("{}/{}.m3u8", app_state.video_path, filename); if let Some(path) = is_valid_full_path(&app_state.base_path, &body.path, false) { if let Ok(child) = create_playlist(path.to_str().unwrap(), &playlist).await { app_state @@ -243,7 +243,7 @@ async fn stream_video( debug!("Playlist: {}", playlist); // Extract video playlist dir to dotenv - if !playlist.starts_with("tmp") + if !playlist.starts_with(&app_state.video_path) && is_valid_full_path(&app_state.base_path, playlist, false).is_some() { HttpResponse::BadRequest().finish() @@ -259,14 +259,19 @@ async fn get_video_part( request: HttpRequest, _: Claims, path: web::Path, + app_state: Data, ) -> impl Responder { let part = &path.path; debug!("Video part: {}", part); - if let Ok(file) = NamedFile::open(String::from("tmp/") + part) { + let mut file_part = PathBuf::new(); + file_part.push(app_state.video_path.clone()); + file_part.push(part); + // TODO: Do we need to guard against directory attacks here? + if let Ok(file) = NamedFile::open(&file_part) { file.into_response(&request) } else { - error!("Video part not found: tmp/{}", part); + error!("Video part not found: {:?}", file_part); HttpResponse::NotFound().finish() } } @@ -450,6 +455,7 @@ fn is_image(entry: &DirEntry) -> bool { .path() .extension() .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_lowercase()) .map(|ext| ext == "jpg" || ext == "jpeg" || ext == "png" || ext == "nef") .unwrap_or(false) } @@ -459,6 +465,7 @@ fn is_video(entry: &DirEntry) -> bool { .path() .extension() .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_lowercase()) .map(|ext| ext == "mp4" || ext == "mov") .unwrap_or(false) } @@ -493,6 +500,13 @@ fn main() -> std::io::Result<()> { .register(Box::new(VIDEO_GAUGE.clone())) .unwrap(); + let app_state = app_data.clone(); + app_state + .playlist_manager + .do_send(ScanDirectoryMessage { + directory: app_state.base_path.clone(), + }); + HttpServer::new(move || { let user_dao = SqliteUserDao::new(); let favorites_dao = SqliteFavoriteDao::new(); diff --git a/src/state.rs b/src/state.rs index 7e2de74..5f6501c 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,11 +1,14 @@ +use crate::video::{PlaylistGenerator, VideoPlaylistManager}; use crate::StreamActor; use actix::{Actor, Addr}; use std::{env, sync::Arc}; pub struct AppState { pub stream_manager: Arc>, + pub playlist_manager: Arc>, pub base_path: String, pub thumbnail_path: String, + pub video_path: String, } impl AppState { @@ -13,11 +16,18 @@ impl AppState { stream_manager: Arc>, base_path: String, thumbnail_path: String, + video_path: String, ) -> Self { + let playlist_generator = PlaylistGenerator::new(); + let video_playlist_manager = + VideoPlaylistManager::new(video_path.clone(), playlist_generator.start()); + Self { stream_manager, + playlist_manager: Arc::new(video_playlist_manager.start()), base_path, thumbnail_path, + video_path, } } } @@ -28,6 +38,7 @@ impl Default for AppState { Arc::new(StreamActor {}.start()), env::var("BASE_PATH").expect("BASE_PATH was not set in the env"), env::var("THUMBNAILS").expect("THUMBNAILS was not set in the env"), + env::var("VIDEO_PATH").expect("VIDEO_PATH was not set in the env"), ) } } diff --git a/src/video.rs b/src/video.rs index 2280260..bd07f1c 100644 --- a/src/video.rs +++ b/src/video.rs @@ -1,10 +1,13 @@ -use std::io::Result; -use std::path::Path; -use std::process::{Child, Command, ExitStatus, Stdio}; - +use crate::is_video; use actix::prelude::*; -use log::{debug, trace}; - +use futures::TryFutureExt; +use log::{debug, info, trace, warn}; +use std::io::Result; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, ExitStatus, Stdio}; +use std::sync::Arc; +use tokio::sync::Semaphore; +use walkdir::{DirEntry, WalkDir}; // ffmpeg -i test.mp4 -c:v h264 -flags +cgop -g 30 -hls_time 3 out.m3u8 // ffmpeg -i "filename.mp4" -preset veryfast -c:v libx264 -f hls -hls_list_size 100 -hls_time 2 -crf 24 -vf scale=1080:-2,setsar=1:1 attempt/vid_out.m3u8 @@ -93,3 +96,215 @@ pub fn generate_video_thumbnail(path: &Path, destination: &Path) { .output() .expect("Failure to create video frame"); } + +pub struct VideoPlaylistManager { + playlist_dir: PathBuf, + playlist_generator: Addr, +} + +impl VideoPlaylistManager { + pub fn new>( + playlist_dir: P, + playlist_generator: Addr, + ) -> Self { + Self { + playlist_dir: playlist_dir.into(), + playlist_generator, + } + } +} + +impl Actor for VideoPlaylistManager { + type Context = Context; +} + +impl Handler for VideoPlaylistManager { + type Result = ResponseFuture<()>; + + fn handle(&mut self, msg: ScanDirectoryMessage, _ctx: &mut Self::Context) -> Self::Result { + let start = std::time::Instant::now(); + info!( + "Starting scan directory for video playlist generation: {}", + msg.directory + ); + + let video_files = WalkDir::new(&msg.directory) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + .filter(|e| is_video(e)) + .collect::>(); + + let scan_dir_name = msg.directory.clone(); + let playlist_output_dir = self.playlist_dir.clone(); + let playlist_generator = self.playlist_generator.clone(); + + Box::pin(async move { + for e in video_files { + let path = e.path(); + let path_as_str = path.to_str().unwrap(); + debug!( + "Sending generate playlist message for path: {}", + path_as_str + ); + + match playlist_generator + .send(GeneratePlaylistMessage { + playlist_path: playlist_output_dir.to_str().unwrap().to_string(), + video_path: PathBuf::from(path), + }) + .await + .expect("Failed to send generate playlist message") + { + Ok(_) => {} + Err(e) => { + warn!("Failed to generate playlist for path '{:?}'. {:?}", path, e); + } + } + // .expect("Failed to generate video playlist"); + } + + info!( + "Finished directory scan of '{}' in {:?}", + scan_dir_name, + start.elapsed() + ); + }) + } +} + +#[derive(Message)] +#[rtype(result = "()")] +pub struct ScanDirectoryMessage { + pub(crate) directory: String, +} + +#[derive(Message)] +#[rtype(result = "Result")] +struct GeneratePlaylistMessage { + video_path: PathBuf, + playlist_path: String, +} + +pub struct PlaylistGenerator { + semaphore: Arc, +} + +impl PlaylistGenerator { + pub(crate) fn new() -> Self { + PlaylistGenerator { + semaphore: Arc::new(Semaphore::new(2)), + } + } +} + +impl Actor for PlaylistGenerator { + type Context = Context; +} + +impl Handler for PlaylistGenerator { + type Result = ResponseFuture>; + + fn handle(&mut self, msg: GeneratePlaylistMessage, _ctx: &mut Self::Context) -> Self::Result { + let video_file = msg.video_path.to_str().unwrap().to_owned(); + let playlist_path = msg.playlist_path.as_str().to_owned(); + let semaphore = self.semaphore.clone(); + + let playlist_file = format!( + "{}/{}.m3u8", + playlist_path, + msg.video_path.file_name().unwrap().to_str().unwrap() + ); + Box::pin(async move { + let wait_start = std::time::Instant::now(); + let permit = semaphore + .acquire_owned() + .await + .expect("Unable to acquire semaphore"); + + debug!( + "Waited for {:?} before starting ffmpeg", + wait_start.elapsed() + ); + + if Path::new(&playlist_file).exists() { + debug!("Playlist already exists: {}", playlist_file); + return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists)); + } + + tokio::spawn(async move { + let ffmpeg_result = tokio::process::Command::new("ffmpeg") + .arg("-i") + .arg(&video_file) + .arg("-c:v") + .arg("h264") + .arg("-crf") + .arg("21") + .arg("-preset") + .arg("veryfast") + .arg("-hls_time") + .arg("3") + .arg("-hls_list_size") + .arg("100") + .arg("-vf") + .arg("scale=1080:-2,setsar=1:1") + .arg(playlist_file) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .status() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) + .await; + + // Hang on to the permit until we're done decoding and then explicitly drop + drop(permit); + + ffmpeg_result + }); + + Ok("meeee".to_string()) + // .spawn() + // .expect("Failed to spawn ffmpeg process"); + + // .expect("Failed to spawn child process") + // .wait() + // .await + // .inspect_err(|e| error!("Failed to wait on child process: {}", e)); + + /* .map(|exit_status| { + debug!( + "Finished waiting for playlist generate process for file '{}' with code: {}", + video_file, + exit_status + ); + + exit_status.to_string() + }) + */ + + /* if let Some(stderr) = ffmpeg.stderr { + tokio::spawn(async move { + let mut reader = BufReader::new(stderr).lines(); + while let Ok(Some(line)) = reader.next_line().await { + println!("ffmpeg line: {:?}", line); + } + }); + }*/ + + /* ffmpeg.wait().await.map(|exit_status| { + debug!( + "Finished waiting for playlist generate process for file '{}' with code: {}", + video_file, exit_status + ); + + exit_status.to_string() + }) + */ + // ffmpeg + // .wait_with_output() + // .await + // .expect("TODO: panic message") + // + // Ok(video_file) + }) + } +}