mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-03 05:34:23 -04:00
Add transmux to the hls playlist
This commit is contained in:
parent
dbe85322ea
commit
63f7a75394
@ -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(),
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user