diff --git a/transcoder/Cargo.lock b/transcoder/Cargo.lock index e2525dd1..bd751f79 100644 --- a/transcoder/Cargo.lock +++ b/transcoder/Cargo.lock @@ -88,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" dependencies = [ "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -203,7 +203,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -398,7 +398,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 1.0.109", ] [[package]] @@ -867,6 +867,20 @@ name = "serde" version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.13", +] [[package]] name = "serde_json" @@ -947,6 +961,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "time" version = "0.3.20" @@ -1047,6 +1072,9 @@ version = "0.1.0" dependencies = [ "actix-files", "actix-web", + "rand", + "serde", + "tokio", ] [[package]] diff --git a/transcoder/Cargo.toml b/transcoder/Cargo.toml index 0842ad02..271a71aa 100644 --- a/transcoder/Cargo.toml +++ b/transcoder/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" [dependencies] actix-web = "4" actix-files = "0.6.2" +tokio = { version = "1.27.0", features = ["process"] } +serde = { version = "1.0.159", features = ["derive"] } +rand = "0.8.5" diff --git a/transcoder/src/main.rs b/transcoder/src/main.rs index 322f02ea..1c9540b8 100644 --- a/transcoder/src/main.rs +++ b/transcoder/src/main.rs @@ -1,20 +1,40 @@ use actix_files::NamedFile; use actix_web::{get, web, App, HttpServer, Result}; + +use crate::transcode::{Quality, TranscoderState}; mod paths; +mod transcode; #[get("/movie/direct/{slug}")] -async fn index(query: web::Path) -> Result { +async fn get_movie_direct(query: web::Path) -> Result { let slug = query.into_inner(); let path = paths::get_movie_path(slug); Ok(NamedFile::open_async(path).await?) - // .map(|f| (infer_content_type(f), f)) +} + +#[get("/movie/{quality}/{slug}")] +async fn get_movie_auto( + query: web::Path<(String, String)>, + state: web::Data, +) -> Result { + let (quality, slug) = query.into_inner(); + let path = paths::get_movie_path(slug); + + Ok(NamedFile::open_async(path).await?) } #[actix_web::main] async fn main() -> std::io::Result<()> { - HttpServer::new(|| App::new().service(index)) - .bind(("0.0.0.0", 7666))? - .run() - .await + let state = web::Data::new(TranscoderState::new()); + + HttpServer::new(move || { + App::new() + .app_data(state.clone()) + .service(get_movie_direct) + .service(get_movie_auto) + }) + .bind(("0.0.0.0", 7666))? + .run() + .await } diff --git a/transcoder/src/transcode.rs b/transcoder/src/transcode.rs new file mode 100644 index 00000000..3b3c07bd --- /dev/null +++ b/transcoder/src/transcode.rs @@ -0,0 +1,94 @@ +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use serde::{Deserialize, Serialize}; +use std::process::{Command, Child}; +use std::{collections::HashMap, sync::Mutex}; + +pub struct TranscoderState { + running: Mutex>, +} + +impl TranscoderState { + pub fn new() -> TranscoderState { + Self { + running: Mutex::new(HashMap::new()), + } + } +} + +pub enum Quality { + P240, + P360, + P480, + P720, + P1080, + P1440, + P4k, + P8k, + Original, +} + +imhttps://sgsot3a.sic.shibaura-it.ac.jp/ + +fn get_transcode_video_quality_args(quality: Quality) -> Vec<&'static str> { + // superfast or ultrafast would produce a file extremly big so we prever veryfast. + let enc_base: Vec<&str> = vec![ + "-map", "0:v:0", "-c:v", "libx264", "-crf", "21", "-preset", "veryfast", + ]; + + match quality { + Quality::Original => vec![], + Quality::P240 => [enc_base, vec!["-vf", "scale=-1:240"]].concat(), + Quality::P360 => [enc_base, vec!["-vf", "scale=-1:360"]].concat(), + Quality::P480 => [enc_base, vec!["-vf", "scale=-1:480"]].concat(), + Quality::P720 => [enc_base, vec!["-vf", "scale=-1:720"]].concat(), + Quality::P1080 => [enc_base, vec!["-vf", "scale=-1:1080"]].concat(), + Quality::P1440 => [enc_base, vec!["-vf", "scale=-1:1440"]].concat(), + Quality::P4k => [enc_base, vec!["-vf", "scale=-1:2160"]].concat(), + Quality::P8k => [enc_base, vec!["-vf", "scale=-1:4320"]].concat(), + } +} + +// TODO: Add audios streams (and transcode them only when necesarry) +async fn start_transcode(path: &str, quality: Quality, start_time_sec: f32) -> (String, Child) { + // TODO: Use the out path below once cached segments can be reused. + // let out_dir = format!("/cache/{show_hash}/{quality}"); + let uuid: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(30) + .map(char::from) + .collect(); + let out_dir = format!("/cache/{uuid}"); + + let segment_time = "10"; + let child = Command::new("ffmpeg") + .args(&["-ss", start_time_sec.to_string().as_str()]) + .args(&["-i", path]) + .args(&["-f", "segment"]) + .args(&["-segment_list_type", "m3u8"]) + // Keep all segments in the list (else only last X are presents, useful for livestreams) + .args(&["--segment_list_size", "0"]) + .args(&["-segment_time", segment_time]) + // Force segments to be exactly segment_time (only works when transcoding) + .args(&[ + "-force_key_frames", + format!("expr:gte(t,n_forced*{segment_time})").as_str(), + "-strict", + "-2", + "-segment_time_delta", + "0.1", + ]) + .args(get_transcode_video_quality_args(quality)) + .args(&[ + "-segment_list".to_string(), + format!("{out_dir}/stream.m3u8"), + format!("{out_dir}/segments-%02d.ts"), + ]) + .spawn() + .expect("ffmpeg failed to start"); + (uuid, child) +} + +pub async fn transcode(user_id: u32, path: &str, quality: Quality, start_time_sec: f32) { + +}