Add transmux to the hls playlist

This commit is contained in:
Zoe Roux 2023-05-28 00:29:55 +09:00
parent dbe85322ea
commit 63f7a75394
3 changed files with 66 additions and 41 deletions

View File

@ -9,55 +9,55 @@ use crate::transcode::Quality;
#[derive(Serialize, ToSchema)] #[derive(Serialize, ToSchema)]
pub struct MediaInfo { pub struct MediaInfo {
/// The length of the media in seconds. /// The length of the media in seconds.
length: f32, pub length: f32,
container: String, pub container: String,
video: VideoTrack, pub video: VideoTrack,
audios: Vec<Track>, pub audios: Vec<Track>,
subtitles: Vec<Track>, pub subtitles: Vec<Track>,
fonts: Vec<String>, pub fonts: Vec<String>,
chapters: Vec<Chapter>, pub chapters: Vec<Chapter>,
} }
#[derive(Serialize, ToSchema)] #[derive(Serialize, ToSchema)]
pub struct VideoTrack { pub struct VideoTrack {
/// The codec of this stream (defined as the RFC 6381). /// 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) /// The language of this stream (as a ISO-639-2 language code)
language: Option<String>, pub language: Option<String>,
/// The max quality of this video track. /// The max quality of this video track.
quality: Quality, pub quality: Quality,
/// The width of the video stream /// The width of the video stream
width: u32, pub width: u32,
/// The height of the video stream /// The height of the video stream
height: u32, pub height: u32,
/// The average bitrate of the video in bytes/s /// The average bitrate of the video in bytes/s
bitrate: u32, pub bitrate: u32,
} }
#[derive(Serialize, ToSchema)] #[derive(Serialize, ToSchema)]
pub struct Track { pub struct Track {
/// The index of this track on the media. /// The index of this track on the media.
index: u32, pub index: u32,
/// The title of the stream. /// The title of the stream.
title: Option<String>, pub title: Option<String>,
/// The language of this stream (as a ISO-639-2 language code) /// The language of this stream (as a ISO-639-2 language code)
language: Option<String>, pub language: Option<String>,
/// The codec of this stream. /// The codec of this stream.
codec: String, pub codec: String,
/// Is this stream the default one of it's type? /// 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) /// Is this stream tagged as forced? (useful only for subtitles)
forced: bool, pub forced: bool,
} }
#[derive(Serialize, ToSchema)] #[derive(Serialize, ToSchema)]
pub struct Chapter { pub struct Chapter {
/// The start time of the chapter (in second from the start of the episode). /// 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). /// 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. /// 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<MediaInfo, std::io::Error> { pub async fn identify(path: String) -> Result<MediaInfo, std::io::Error> {
@ -80,7 +80,6 @@ pub async fn identify(path: String) -> Result<MediaInfo, std::io::Error> {
v.as_str().and_then(|x| x.parse::<F>().ok()) v.as_str().and_then(|x| x.parse::<F>().ok())
} }
// TODO: Every number is wrapped by "" in mediainfo json's mode so the number parsing is wrong.
Ok(MediaInfo { Ok(MediaInfo {
length: parse::<f32>(&general["Duration"]).unwrap(), length: parse::<f32>(&general["Duration"]).unwrap(),
container: general["Format"].as_str().unwrap().to_string(), container: general["Format"].as_str().unwrap().to_string(),

View File

@ -68,7 +68,10 @@ async fn get_master(
transcoder: web::Data<Transcoder>, transcoder: web::Data<Transcoder>,
) -> Result<String, ApiError> { ) -> Result<String, ApiError> {
let (resource, slug) = query.into_inner(); let (resource, slug) = query.into_inner();
Ok(transcoder.build_master(resource, slug).await) transcoder
.build_master(resource, slug)
.await
.ok_or(ApiError::InternalError)
} }
/// Identify /// Identify
@ -84,7 +87,7 @@ async fn get_master(
("slug" = String, Path, description = "The slug of the movie/episode."), ("slug" = String, Path, description = "The slug of the movie/episode."),
) )
)] )]
#[get("/{resource}/{slug}/identify")] #[get("/{resource}/{slug}/info")]
async fn identify_resource( async fn identify_resource(
query: web::Path<(String, String)>, query: web::Path<(String, String)>,
) -> Result<Json<MediaInfo>, ApiError> { ) -> Result<Json<MediaInfo>, ApiError> {
@ -94,7 +97,10 @@ async fn identify_resource(
.map_err(|_| ApiError::NotFound)?; .map_err(|_| ApiError::NotFound)?;
identify(path).await.map(|info| Json(info)).map_err(|e| { 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 ApiError::InternalError
}) })
} }

View File

@ -1,5 +1,7 @@
use crate::transcode::*; use crate::identify::identify;
use crate::paths::get_path;
use crate::utils::Signalable; use crate::utils::Signalable;
use crate::transcode::*;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::RwLock; 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<String> {
let mut master = String::from("#EXTM3U\n"); let mut master = String::from("#EXTM3U\n");
// TODO: Add transmux (original quality) in this master playlist. let path = get_path(resource, slug).await.ok()?;
// Transmux should be the first variant since it's used to test bandwidth let info = identify(path).await.ok()?;
// and serve as a hint for preffered variant for clients.
// TODO: Fetch kyoo to retrieve the max quality and the aspect_ratio // TODO: Only add this if transmuxing is possible.
let aspect_ratio = 16.0 / 9.0; if true {
for quality in Quality::iter() { // 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 // 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("#EXT-X-STREAM-INF:");
master.push_str(format!("AVERAGE-BANDWIDTH={},", quality.average_bitrate()).as_str()); master.push_str(format!("AVERAGE-BANDWIDTH={},", quality.average_bitrate()).as_str());
@ -39,26 +56,29 @@ impl Transcoder {
.as_str(), .as_str(),
); );
master.push_str("CODECS=\"avc1.640028\","); 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("AUDIO=\"audio\"\n");
master.push_str(format!("./{}/index.m3u8\n", quality).as_str()); master.push_str(format!("./{}/index.m3u8\n", quality).as_str());
} }
// TODO: Fetch audio stream list/metadata from kyoo. for audio in info.audios {
for audio in vec![0] {
// Doc: https://developer.apple.com/documentation/http_live_streaming/example_playlists_for_http_live_streaming/adding_alternate_media_to_a_playlist // 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,"); master.push_str("#EXT-X-MEDIA:TYPE=AUDIO,");
// The group-id allows to distinguish multiple qualities from multiple variants. // The group-id allows to distinguish multiple qualities from multiple variants.
// We could create another quality set and use group-ids hiqual and lowqual. // We could create another quality set and use group-ids hiqual and lowqual.
master.push_str("GROUP-ID=\"audio\","); master.push_str("GROUP-ID=\"audio\",");
// master.push_str(format!("LANGUAGE=\"{}\",", "eng").as_str()); if let Some(language) = audio.language {
master.push_str(format!("NAME=\"{}\",", "Default").as_str()); 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) // TODO: Support aac5.1 (and specify the number of channel bellow)
// master.push_str(format!("CHANNELS=\"{}\",", 2).as_str()); // master.push_str(format!("CHANNELS=\"{}\",", 2).as_str());
master.push_str("DEFAULT=YES,"); 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( pub async fn transcode(