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)]
pub struct MediaInfo {
/// The length of the media in seconds.
length: f32,
container: String,
video: VideoTrack,
audios: Vec<Track>,
subtitles: Vec<Track>,
fonts: Vec<String>,
chapters: Vec<Chapter>,
pub length: f32,
pub container: String,
pub video: VideoTrack,
pub audios: Vec<Track>,
pub subtitles: Vec<Track>,
pub fonts: Vec<String>,
pub chapters: Vec<Chapter>,
}
#[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<String>,
pub language: Option<String>,
/// 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<String>,
pub title: Option<String>,
/// The language of this stream (as a ISO-639-2 language code)
language: Option<String>,
pub language: Option<String>,
/// 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<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())
}
// TODO: Every number is wrapped by "" in mediainfo json's mode so the number parsing is wrong.
Ok(MediaInfo {
length: parse::<f32>(&general["Duration"]).unwrap(),
container: general["Format"].as_str().unwrap().to_string(),

View File

@ -68,7 +68,10 @@ async fn get_master(
transcoder: web::Data<Transcoder>,
) -> Result<String, ApiError> {
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<Json<MediaInfo>, 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
})
}

View File

@ -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<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.
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(