diff --git a/transcoder/src/identify.rs b/transcoder/src/identify.rs index dc83f3e3..78ee8ec7 100644 --- a/transcoder/src/identify.rs +++ b/transcoder/src/identify.rs @@ -9,55 +9,55 @@ use crate::transcode::Quality; #[derive(Serialize, ToSchema)] pub struct MediaInfo { /// The length of the media in seconds. - length: f32, - container: String, - video: VideoTrack, - audios: Vec, - subtitles: Vec, - fonts: Vec, - chapters: Vec, + pub length: f32, + pub container: String, + pub video: VideoTrack, + pub audios: Vec, + pub subtitles: Vec, + pub fonts: Vec, + pub chapters: Vec, } #[derive(Serialize, ToSchema)] pub struct VideoTrack { /// The codec of this stream (defined as the RFC 6381). - codec: String, + pub codec: String, /// The language of this stream (as a ISO-639-2 language code) - language: Option, + pub language: Option, /// The max quality of this video track. - quality: Quality, + pub quality: Quality, /// The width of the video stream - width: u32, + pub width: u32, /// The height of the video stream - height: u32, + pub height: u32, /// The average bitrate of the video in bytes/s - bitrate: u32, + pub bitrate: u32, } #[derive(Serialize, ToSchema)] pub struct Track { /// The index of this track on the media. - index: u32, + pub index: u32, /// The title of the stream. - title: Option, + pub title: Option, /// The language of this stream (as a ISO-639-2 language code) - language: Option, + pub language: Option, /// The codec of this stream. - codec: String, + pub codec: String, /// Is this stream the default one of it's type? - default: bool, + pub default: bool, /// Is this stream tagged as forced? (useful only for subtitles) - forced: bool, + pub forced: bool, } #[derive(Serialize, ToSchema)] pub struct Chapter { /// The start time of the chapter (in second from the start of the episode). - start: f32, + pub start: f32, /// The end time of the chapter (in second from the start of the episode). - end: f32, + pub end: f32, /// The name of this chapter. This should be a human-readable name that could be presented to the user. - name: String, // TODO: add a type field for Opening, Credits... + pub name: String, // TODO: add a type field for Opening, Credits... } pub async fn identify(path: String) -> Result { @@ -80,7 +80,6 @@ pub async fn identify(path: String) -> Result { v.as_str().and_then(|x| x.parse::().ok()) } - // TODO: Every number is wrapped by "" in mediainfo json's mode so the number parsing is wrong. Ok(MediaInfo { length: parse::(&general["Duration"]).unwrap(), container: general["Format"].as_str().unwrap().to_string(), diff --git a/transcoder/src/main.rs b/transcoder/src/main.rs index 42f05aa6..949c7841 100644 --- a/transcoder/src/main.rs +++ b/transcoder/src/main.rs @@ -68,7 +68,10 @@ async fn get_master( transcoder: web::Data, ) -> Result { let (resource, slug) = query.into_inner(); - Ok(transcoder.build_master(resource, slug).await) + transcoder + .build_master(resource, slug) + .await + .ok_or(ApiError::InternalError) } /// Identify @@ -84,7 +87,7 @@ async fn get_master( ("slug" = String, Path, description = "The slug of the movie/episode."), ) )] -#[get("/{resource}/{slug}/identify")] +#[get("/{resource}/{slug}/info")] async fn identify_resource( query: web::Path<(String, String)>, ) -> Result, ApiError> { @@ -94,7 +97,10 @@ async fn identify_resource( .map_err(|_| ApiError::NotFound)?; identify(path).await.map(|info| Json(info)).map_err(|e| { - eprintln!("Unhandled error occured while identifing the resource: {}", e); + eprintln!( + "Unhandled error occured while identifing the resource: {}", + e + ); ApiError::InternalError }) } diff --git a/transcoder/src/state.rs b/transcoder/src/state.rs index ff2d18b9..2cdbc26b 100644 --- a/transcoder/src/state.rs +++ b/transcoder/src/state.rs @@ -1,5 +1,7 @@ -use crate::transcode::*; +use crate::identify::identify; +use crate::paths::get_path; use crate::utils::Signalable; +use crate::transcode::*; use std::collections::HashMap; use std::path::PathBuf; use std::sync::RwLock; @@ -17,15 +19,30 @@ impl Transcoder { } } - pub async fn build_master(&self, _resource: String, _slug: String) -> String { + pub async fn build_master(&self, resource: String, slug: String) -> Option { let mut master = String::from("#EXTM3U\n"); - // TODO: Add transmux (original quality) in this master playlist. - // Transmux should be the first variant since it's used to test bandwidth - // and serve as a hint for preffered variant for clients. + let path = get_path(resource, slug).await.ok()?; + let info = identify(path).await.ok()?; - // TODO: Fetch kyoo to retrieve the max quality and the aspect_ratio - let aspect_ratio = 16.0 / 9.0; - for quality in Quality::iter() { + // TODO: Only add this if transmuxing is possible. + if true { + // Doc: https://developer.apple.com/documentation/http_live_streaming/example_playlists_for_http_live_streaming/creating_a_multivariant_playlist + master.push_str("#EXT-X-STREAM-INF:"); + master.push_str(format!("AVERAGE-BANDWIDTH={},", info.video.bitrate).as_str()); + // Approximate a bit more because we can't know the maximum bandwidth. + master.push_str(format!("BANDWIDTH={},", (info.video.bitrate as f32 * 1.2) as u32).as_str()); + master.push_str( + format!("RESOLUTION={}x{},", info.video.width, info.video.height).as_str(), + ); + // TODO: Find codecs in the RFC 6381 format. + // master.push_str("CODECS=\"avc1.640028\","); + // TODO: With multiple audio qualities, maybe switch qualities depending on the video quality. + master.push_str("AUDIO=\"audio\"\n"); + master.push_str(format!("./{}/index.m3u8\n", Quality::Original).as_str()); + } + + let aspect_ratio = info.video.width as f32 / info.video.height as f32; + for quality in Quality::iter().filter(|x| x.height() <= info.video.quality.height()) { // Doc: https://developer.apple.com/documentation/http_live_streaming/example_playlists_for_http_live_streaming/creating_a_multivariant_playlist master.push_str("#EXT-X-STREAM-INF:"); master.push_str(format!("AVERAGE-BANDWIDTH={},", quality.average_bitrate()).as_str()); @@ -39,26 +56,29 @@ impl Transcoder { .as_str(), ); master.push_str("CODECS=\"avc1.640028\","); - // With multiple audio qualities, maybe switch qualities depending on the video quality. + // TODO: With multiple audio qualities, maybe switch qualities depending on the video quality. master.push_str("AUDIO=\"audio\"\n"); master.push_str(format!("./{}/index.m3u8\n", quality).as_str()); } - // TODO: Fetch audio stream list/metadata from kyoo. - for audio in vec![0] { + for audio in info.audios { // Doc: https://developer.apple.com/documentation/http_live_streaming/example_playlists_for_http_live_streaming/adding_alternate_media_to_a_playlist master.push_str("#EXT-X-MEDIA:TYPE=AUDIO,"); // The group-id allows to distinguish multiple qualities from multiple variants. // We could create another quality set and use group-ids hiqual and lowqual. master.push_str("GROUP-ID=\"audio\","); - // master.push_str(format!("LANGUAGE=\"{}\",", "eng").as_str()); - master.push_str(format!("NAME=\"{}\",", "Default").as_str()); + if let Some(language) = audio.language { + master.push_str(format!("LANGUAGE=\"{}\",", language).as_str()); + } + if let Some(title) = audio.title { + master.push_str(format!("NAME=\"{}\",", title).as_str()); + } // TODO: Support aac5.1 (and specify the number of channel bellow) // master.push_str(format!("CHANNELS=\"{}\",", 2).as_str()); master.push_str("DEFAULT=YES,"); - master.push_str(format!("URI=\"./audio/{}/index.m3u8\"\n", audio).as_str()); + master.push_str(format!("URI=\"./audio/{}/index.m3u8\"\n", audio.index).as_str()); } - master + Some(master) } pub async fn transcode(