diff --git a/transcoder/Cargo.lock b/transcoder/Cargo.lock index b8004086..ae6510ff 100644 --- a/transcoder/Cargo.lock +++ b/transcoder/Cargo.lock @@ -640,6 +640,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown", + "serde", ] [[package]] @@ -678,6 +679,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.141" @@ -839,6 +846,30 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.56" @@ -1263,6 +1294,7 @@ dependencies = [ "reqwest", "serde", "tokio", + "utoipa", ] [[package]] @@ -1324,6 +1356,32 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utoipa" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ae74ef183fae36d650f063ae7bde1cacbe1cd7e72b617cbe1e985551878b98" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea8ac818da7e746a63285594cce8a96f5e00ee31994e655bd827569cb8b137b" +dependencies = [ + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 2.0.13", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/transcoder/Cargo.toml b/transcoder/Cargo.toml index 4c236f8d..88d9c1d7 100644 --- a/transcoder/Cargo.toml +++ b/transcoder/Cargo.toml @@ -11,3 +11,4 @@ serde = { version = "1.0.159", features = ["derive"] } rand = "0.8.5" derive_more = "0.99.17" reqwest = { version = "0.11.16", default_features = false, features = ["json", "rustls-tls"] } +utoipa = { version = "3", features = ["actix_extras"] } diff --git a/transcoder/src/identify.rs b/transcoder/src/identify.rs index 199f0b20..65326fb7 100644 --- a/transcoder/src/identify.rs +++ b/transcoder/src/identify.rs @@ -1,6 +1,7 @@ use serde::Serialize; +use utoipa::ToSchema; -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct MediaInfo { container: String, video_codec: String, @@ -10,7 +11,7 @@ pub struct MediaInfo { chapters: Vec, } -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct Track { /// The index of this track on the media. index: u32, @@ -26,7 +27,7 @@ pub struct Track { forced: bool, } -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct Chapter { /// The start time of the chapter (in second from the start of the episode). start: f32, @@ -37,6 +38,6 @@ pub struct Chapter { // TODO: add a type field for Opening, Credits... } -pub fn identify(path: String) -> Result { +pub fn identify(_path: String) -> Result { todo!() } diff --git a/transcoder/src/main.rs b/transcoder/src/main.rs index 235e8fad..e8df8f04 100644 --- a/transcoder/src/main.rs +++ b/transcoder/src/main.rs @@ -7,9 +7,10 @@ use actix_web::{ App, HttpRequest, HttpServer, Result, }; use error::ApiError; +use utoipa::OpenApi; use crate::{ - identify::{identify, MediaInfo}, + identify::{identify, MediaInfo, Track, Chapter}, transcode::{Quality, Transcoder}, }; mod error; @@ -24,7 +25,21 @@ fn get_client_id(req: HttpRequest) -> Result { .map(|x| x.to_str().unwrap().to_string()) } -#[get("/{resource}/{slug}/direct{extension}")] +/// Direct video +/// +/// Retrieve the raw video stream, in the same container as the one on the server. No transcoding or +/// transmuxing is done. +#[utoipa::path( + responses( + (status = 200, description = "The item is returned"), + (status = NOT_FOUND, description = "Invalid slug.") + ), + params( + ("resource" = String, Path, description = "Episode or movie"), + ("slug" = String, Path, description = "The slug of the movie/episode."), + ) +)] +#[get("/{resource}/{slug}/direct")] async fn get_direct(query: web::Path<(String, String)>) -> Result { let (resource, slug) = query.into_inner(); let path = paths::get_path(resource, slug).await.map_err(|e| { @@ -35,6 +50,23 @@ async fn get_direct(query: web::Path<(String, String)>) -> Result { Ok(NamedFile::open_async(path).await?) } +/// Transcode video +/// +/// Transcode the video to the selected quality. +/// This route can take a few seconds to respond since it will way for at least one segment to be +/// available. +#[utoipa::path( + responses( + (status = 200, description = "Get the m3u8 playlist."), + (status = NOT_FOUND, description = "Invalid slug.") + ), + params( + ("resource" = String, Path, description = "Episode or movie"), + ("slug" = String, Path, description = "The slug of the movie/episode."), + ("quality" = Quality, Path, description = "Specify the quality you want"), + ("x-client-id" = String, Header, description = "A unique identify for a player's instance. Used to cancel unused transcode"), + ) +)] #[get("/{resource}/{slug}/{quality}/index.m3u8")] async fn get_transcoded( req: HttpRequest, @@ -60,6 +92,22 @@ async fn get_transcoded( }) } +/// Get transmuxed chunk +/// +/// Retrieve a chunk of a transmuxed video. +#[utoipa::path( + responses( + (status = 200, description = "Get a hls chunk."), + (status = NOT_FOUND, description = "Invalid slug.") + ), + params( + ("resource" = String, Path, description = "Episode or movie"), + ("slug" = String, Path, description = "The slug of the movie/episode."), + ("quality" = Quality, Path, description = "Specify the quality you want"), + ("chunk" = u32, Path, description = "The number of the chunk"), + ("x-client-id" = String, Header, description = "A unique identify for a player's instance. Used to cancel unused transcode"), + ) +)] #[get("/{resource}/{slug}/{quality}/segments-{chunk}.ts")] async fn get_chunk( req: HttpRequest, @@ -89,6 +137,19 @@ async fn get_chunk( }) } +/// Identify +/// +/// Identify metadata about a file +#[utoipa::path( + responses( + (status = 200, description = "Ok", body = MediaInfo), + (status = NOT_FOUND, description = "Invalid slug.") + ), + params( + ("resource" = String, Path, description = "Episode or movie"), + ("slug" = String, Path, description = "The slug of the movie/episode."), + ) +)] #[get("/{resource}/{slug}/identify")] async fn identify_resource( query: web::Path<(String, String)>, @@ -104,6 +165,19 @@ async fn identify_resource( }) } +#[get("/openapi.json")] +async fn get_swagger() -> String { + #[derive(OpenApi)] + #[openapi( + info(description = "Transcoder's open api."), + paths(get_direct, get_transcoded, get_chunk, identify_resource), + components(schemas(MediaInfo, Track, Chapter)) + )] + struct ApiDoc; + + ApiDoc::openapi().to_pretty_json().unwrap() +} + #[actix_web::main] async fn main() -> std::io::Result<()> { let state = web::Data::new(Transcoder::new()); @@ -115,6 +189,7 @@ async fn main() -> std::io::Result<()> { .service(get_transcoded) .service(get_chunk) .service(identify_resource) + .service(get_swagger) }) .bind(("0.0.0.0", 7666))? .run()