diff --git a/front/packages/ui/src/player/components/right-buttons.tsx b/front/packages/ui/src/player/components/right-buttons.tsx index a5bf6fe7..fdd9f8e1 100644 --- a/front/packages/ui/src/player/components/right-buttons.tsx +++ b/front/packages/ui/src/player/components/right-buttons.tsx @@ -41,7 +41,7 @@ export const RightButtons = ({ }: { subtitles?: Track[]; fonts?: Font[]; - qualities?: WatchItem["link"] + qualities?: WatchItem["link"]; onMenuOpen: () => void; onMenuClose: () => void; } & Stylable) => { diff --git a/transcoder/src/identify.rs b/transcoder/src/identify.rs index aa46915e..00ddd1a4 100644 --- a/transcoder/src/identify.rs +++ b/transcoder/src/identify.rs @@ -17,12 +17,21 @@ pub struct MediaInfo { #[derive(Serialize, ToSchema)] pub struct VideoTrack { - /// The codec of this stream. + /// The codec of this stream (defined as the RFC 6381). codec: String, /// The language of this stream (as a ISO-639-2 language code) language: String, /// The max quality of this video track. quality: Quality, + /// The width of the video stream + width: u32, + /// The height of the video stream + height: u32, + /// The average bitrate of the video in bytes/s + average_bitrate: u32, + // TODO: Figure out if this is doable + /// The max bitrate of the video in bytes/s + max_bitrate: u32, } #[derive(Serialize, ToSchema)] diff --git a/transcoder/src/main.rs b/transcoder/src/main.rs index 5b83a2ed..ee8f6209 100644 --- a/transcoder/src/main.rs +++ b/transcoder/src/main.rs @@ -1,4 +1,4 @@ -use std::{str::FromStr, iter::Map, collections::HashMap}; +use std::str::FromStr; use actix_files::NamedFile; use actix_web::{ @@ -68,17 +68,10 @@ async fn get_direct(query: web::Path<(String, String)>) -> Result { #[get("/{resource}/{slug}/master.m3u8")] async fn get_master( query: web::Path<(String, String)>, + transcoder: web::Data, ) -> Result { - let (_resource, _slug) = query.into_inner(); - - // TODO: Fetch kyoo to retrieve the max quality (as well as the list of audio streams) - // TODO: Put resolutions based on the aspect ratio and do not assume 16/9 - Ok(String::from(r#"#EXTM3U -#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=400000,BANDWIDTH=700000,RESOLUTION=426x240,CODECS="avc1.640028" -./240p/index.m3u8 -#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2400000,BANDWIDTH=4000000,RESOLUTION=1080x720,CODECS="avc1.640028" -./720p/index.m3u8 -"#)) + let (resource, slug) = query.into_inner(); + Ok(transcoder.build_master(resource, slug).await) } /// Transcode video @@ -227,3 +220,4 @@ async fn main() -> std::io::Result<()> { .run() .await } + diff --git a/transcoder/src/transcode.rs b/transcoder/src/transcode.rs index dfd82796..9b2c3b38 100644 --- a/transcoder/src/transcode.rs +++ b/transcoder/src/transcode.rs @@ -1,9 +1,11 @@ +use derive_more::Display; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; use serde::Serialize; use std::collections::HashMap; use std::path::PathBuf; use std::process::Stdio; +use std::slice::Iter; use std::str::FromStr; use std::sync::RwLock; use tokio::io::{AsyncBufReadExt, BufReader}; @@ -12,19 +14,89 @@ use tokio::sync::watch::{self, Receiver}; use crate::utils::Signalable; -#[derive(PartialEq, Eq, Serialize)] +#[derive(PartialEq, Eq, Serialize, Display)] pub enum Quality { + #[display(fmt = "240p")] P240, + #[display(fmt = "360p")] P360, + #[display(fmt = "480p")] P480, + #[display(fmt = "720p")] P720, + #[display(fmt = "1080p")] P1080, + #[display(fmt = "1440p")] P1440, + #[display(fmt = "4k")] P4k, + #[display(fmt = "8k")] P8k, + #[display(fmt = "original")] Original, } +impl Quality { + fn iter() -> Iter<'static, Quality> { + static QUALITIES: [Quality; 8] = [ + Quality::P240, + Quality::P360, + Quality::P480, + Quality::P720, + Quality::P1080, + Quality::P1440, + Quality::P4k, + Quality::P8k, + // Purposfully removing Original from this list (since it require special treatments + // anyways) + ]; + QUALITIES.iter() + } + + fn height(&self) -> u32 { + match self { + Self::P240 => 240, + Self::P360 => 360, + Self::P480 => 480, + Self::P720 => 720, + Self::P1080 => 1080, + Self::P1440 => 1440, + Self::P4k => 2160, + Self::P8k => 4320, + Self::Original => panic!("Original quality must be handled specially"), + } + } + + // I'm not entierly sure about the values for bitrates. Double checking would be nice. + fn average_bitrate(&self) -> u32 { + match self { + Self::P240 => 400_000, + Self::P360 => 800_000, + Self::P480 => 1200_000, + Self::P720 => 2400_000, + Self::P1080 => 4800_000, + Self::P1440 => 9600_000, + Self::P4k => 16_000_000, + Self::P8k => 28_000_000, + Self::Original => panic!("Original quality must be handled specially"), + } + } + + fn max_bitrate(&self) -> u32 { + match self { + Self::P240 => 700_000, + Self::P360 => 1400_000, + Self::P480 => 2100_000, + Self::P720 => 4000_000, + Self::P1080 => 8000_000, + Self::P1440 => 12_000_000, + Self::P4k => 28_000_000, + Self::P8k => 40_000_000, + Self::Original => panic!("Original quality must be handled specially"), + } + } +} + #[derive(Debug, PartialEq, Eq)] pub struct InvalidValueError; @@ -47,66 +119,41 @@ impl FromStr for Quality { } } -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", - ]; - - // I'm not entierly sure about the values for bitrates. Double checking would be nice. - // Even less sure but bufsize are 5x the avergae bitrate since the average bitrate is only - // useful for hls segments. - match quality { - Quality::Original => vec![], - Quality::P240 => [ - enc_base, - vec!["-vf", "scale=-2:'min(240,ih)'"], - vec!["-b:v", "400k", "-bufsize", "2000k", "-maxrate", "700k"], - ] - .concat(), - Quality::P360 => [ - enc_base, - vec!["-vf", "scale=-2:'min(360,ih)'"], - vec!["-b:v", "800k", "-bufsize", "4000k", "-maxrate", "1400"], - ] - .concat(), - Quality::P480 => [ - enc_base, - vec!["-vf", "scale=-2:'min(480,ih)'"], - vec!["-b:v", "1200k", "-bufsize", "6000k", "-maxrate", "2100k"], - ] - .concat(), - Quality::P720 => [ - enc_base, - vec!["-vf", "scale=-2:'min(720,ih)'"], - vec!["-b:v", "2400k", "-bufsize", "12000k", "-maxrate", "4000k"], - ] - .concat(), - Quality::P1080 => [ - enc_base, - vec!["-vf", "scale=-2:'min(1080,ih)'"], - vec!["-b:v", "4800k", "-bufsize", "24000k", "-maxrate", "6000k"], - ] - .concat(), - Quality::P1440 => [ - enc_base, - vec!["-vf", "scale=-2:'min(1440,ih)'"], - vec!["-b:v", "9600k", "-bufsize", "48000k", "-maxrate", "12000k"], - ] - .concat(), - Quality::P4k => [ - enc_base, - vec!["-vf", "scale=-2:'min(2160,ih)'"], - vec!["-b:v", "16000k", "-bufsize", "80000k", "-maxrate", "28000k"], - ] - .concat(), - Quality::P8k => [ - enc_base, - vec!["-vf", "scale=-2:'min(4320,ih)'"], - vec!["-b:v", "28000k", "-bufsize", "140000k", "-maxrate", "40000k"], - ] - .concat(), +fn get_transcode_video_quality_args(quality: &Quality, segment_time: u32) -> Vec { + if *quality == Quality::Original { + return vec!["-map", "0:v:0", "-c:v", "copy"] + .iter() + .map(|a| a.to_string()) + .collect(); } + vec![ + // superfast or ultrafast would produce a file extremly big so we prever veryfast. + vec![ + "-map", "0:v:0", "-c:v", "libx264", "-crf", "21", "-preset", "veryfast", + ], + vec![ + "-vf", + format!("scale=-2:'min({height},ih)'", height = quality.height()).as_str(), + ], + // Even less sure but bufsize are 5x the avergae bitrate since the average bitrate is only + // useful for hls segments. + vec!["-bufsize", (quality.max_bitrate() * 5).to_string().as_str()], + vec!["-b:v", quality.average_bitrate().to_string().as_str()], + vec!["-maxrate", quality.max_bitrate().to_string().as_str()], + // Force segments to be exactly segment_time (only works when transcoding) + vec![ + "-force_key_frames", + format!("expr:gte(t,n_forced*{segment_time})").as_str(), + "-strict", + "-2", + "-segment_time_delta", + "0.1", + ], + ] + .concat() + .iter() + .map(|arg| arg.to_string()) + .collect() } // TODO: Add audios streams (and transcode them only when necesarry) @@ -130,20 +177,12 @@ async fn start_transcode(path: String, quality: Quality, start_time: u32) -> Tra .args(&["-f", "hls"]) // Use a .tmp file for segments (.ts files) .args(&["-hls_flags", "temp_file"]) - .args(&["-hls_allow_cache", "1"]) + // Cache can't be allowed since switching quality means starting a new encode for now. + // .args(&["-hls_allow_cache", "1"]) // Keep all segments in the list (else only last X are presents, useful for livestreams) .args(&["-hls_list_size", "0"]) .args(&["-hls_time", segment_time.to_string().as_str()]) - // 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(get_transcode_video_quality_args(&quality, segment_time)) .args(&[ "-hls_segment_filename".to_string(), format!("{out_dir}/segments-%02d.ts"), @@ -218,6 +257,33 @@ impl Transcoder { } } + pub async fn build_master(&self, _resource: String, _slug: String) -> String { + 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. + + // TODO: Fetch kyoo to retrieve the max quality and the aspect_ratio + let aspect_ratio = 16.0 / 9.0; + for quality in Quality::iter() { + master.push_str("#EXT-X-STREAM-INF:"); + master.push_str(format!("AVERAGE-BANDWIDTH={},", quality.average_bitrate()).as_str()); + master.push_str(format!("BANDWIDTH={},", quality.max_bitrate()).as_str()); + master.push_str( + format!( + "RESOLUTION={}x{},", + (aspect_ratio * quality.height() as f32).round() as u32, + quality.height() + ) + .as_str(), + ); + master.push_str("CODECS=\"avc1.640028\"\n"); + master.push_str(format!("./{}/index.m3u8\n", quality).as_str()); + } + // TODO: Add audio streams + master + } + pub async fn transcode( &self, client_id: String,