mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-30 19:54:16 -04:00
Add routes for audio streams
This commit is contained in:
parent
5ee0a0044a
commit
0b2d8a7e2e
77
transcoder/src/audio.rs
Normal file
77
transcoder/src/audio.rs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
use crate::{
|
||||||
|
error::ApiError,
|
||||||
|
paths,
|
||||||
|
transcode::Transcoder,
|
||||||
|
};
|
||||||
|
use actix_files::NamedFile;
|
||||||
|
use actix_web::{get, web, Result};
|
||||||
|
|
||||||
|
/// Transcode audio
|
||||||
|
///
|
||||||
|
/// Get the selected audio
|
||||||
|
/// This route can take a few seconds to respond since it will way for at least one segment to be
|
||||||
|
/// available.
|
||||||
|
#[utoipa::path(
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get the m3u8 playlist."),
|
||||||
|
(status = NOT_FOUND, description = "Invalid slug.")
|
||||||
|
),
|
||||||
|
params(
|
||||||
|
("resource" = String, Path, description = "Episode or movie"),
|
||||||
|
("slug" = String, Path, description = "The slug of the movie/episode."),
|
||||||
|
("audio" = u32, Path, description = "Specify the audio stream you want. For mappings, refer to the audios fields of the /watch response."),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#[get("/audio/{resource}/{slug}/{audio}/index.m3u8")]
|
||||||
|
async fn get_audio_transcoded(
|
||||||
|
query: web::Path<(String, String, u32)>,
|
||||||
|
transcoder: web::Data<Transcoder>,
|
||||||
|
) -> Result<String, ApiError> {
|
||||||
|
let (resource, slug, audio) = query.into_inner();
|
||||||
|
let path = paths::get_path(resource, slug)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::NotFound)?;
|
||||||
|
|
||||||
|
transcoder
|
||||||
|
.get_transcoded_audio(path, audio)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::InternalError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get audio chunk
|
||||||
|
///
|
||||||
|
/// Retrieve a chunk of a transcoded audio.
|
||||||
|
#[utoipa::path(
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get a hls chunk."),
|
||||||
|
(status = NOT_FOUND, description = "Invalid slug.")
|
||||||
|
),
|
||||||
|
params(
|
||||||
|
("resource" = String, Path, description = "Episode or movie"),
|
||||||
|
("slug" = String, Path, description = "The slug of the movie/episode."),
|
||||||
|
("audio" = u32, Path, description = "Specify the audio you want"),
|
||||||
|
("chunk" = u32, Path, description = "The number of the chunk"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#[get("/audio/{resource}/{slug}/{audio}/segments-{chunk}.ts")]
|
||||||
|
async fn get_audio_chunk(
|
||||||
|
query: web::Path<(String, String, u32, u32)>,
|
||||||
|
transcoder: web::Data<Transcoder>,
|
||||||
|
) -> Result<NamedFile, ApiError> {
|
||||||
|
let (resource, slug, audio, chunk) = query.into_inner();
|
||||||
|
let path = paths::get_path(resource, slug)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::NotFound)?;
|
||||||
|
|
||||||
|
transcoder
|
||||||
|
.get_audio_segment(path, audio, chunk)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::BadRequest {
|
||||||
|
error: "No transcode started for the selected show/audio.".to_string(),
|
||||||
|
})
|
||||||
|
.and_then(|path| {
|
||||||
|
NamedFile::open(path).map_err(|_| ApiError::BadRequest {
|
||||||
|
error: "Invalid segment number.".to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -1,29 +1,25 @@
|
|||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use actix_files::NamedFile;
|
use actix_files::NamedFile;
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
get,
|
get,
|
||||||
web::{self, Json},
|
web::{self, Json},
|
||||||
App, HttpRequest, HttpServer, Result,
|
App, HttpServer, Result,
|
||||||
};
|
};
|
||||||
use error::ApiError;
|
use error::ApiError;
|
||||||
use utoipa::OpenApi;
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
audio::*,
|
||||||
identify::{identify, Chapter, MediaInfo, Track},
|
identify::{identify, Chapter, MediaInfo, Track},
|
||||||
transcode::{Quality, Transcoder},
|
transcode::Transcoder,
|
||||||
|
video::*,
|
||||||
};
|
};
|
||||||
|
mod audio;
|
||||||
mod error;
|
mod error;
|
||||||
mod identify;
|
mod identify;
|
||||||
mod paths;
|
mod paths;
|
||||||
mod transcode;
|
mod transcode;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
mod video;
|
||||||
fn get_client_id(req: HttpRequest) -> Result<String, ApiError> {
|
|
||||||
req.headers().get("x-client-id")
|
|
||||||
.ok_or(ApiError::BadRequest { error: String::from("Missing client id. Please specify the X-CLIENT-ID header to a guid constant for the lifetime of the player (but unique per instance)."), })
|
|
||||||
.map(|x| x.to_str().unwrap().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Direct video
|
/// Direct video
|
||||||
///
|
///
|
||||||
@ -74,93 +70,6 @@ async fn get_master(
|
|||||||
Ok(transcoder.build_master(resource, slug).await)
|
Ok(transcoder.build_master(resource, slug).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transcode video
|
|
||||||
///
|
|
||||||
/// Transcode the video to the selected quality.
|
|
||||||
/// This route can take a few seconds to respond since it will way for at least one segment to be
|
|
||||||
/// available.
|
|
||||||
#[utoipa::path(
|
|
||||||
responses(
|
|
||||||
(status = 200, description = "Get the m3u8 playlist."),
|
|
||||||
(status = NOT_FOUND, description = "Invalid slug.")
|
|
||||||
),
|
|
||||||
params(
|
|
||||||
("resource" = String, Path, description = "Episode or movie"),
|
|
||||||
("slug" = String, Path, description = "The slug of the movie/episode."),
|
|
||||||
("quality" = Quality, Path, description = "Specify the quality you want"),
|
|
||||||
("x-client-id" = String, Header, description = "A unique identify for a player's instance. Used to cancel unused transcode"),
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
#[get("/{resource}/{slug}/{quality}/index.m3u8")]
|
|
||||||
async fn get_transcoded(
|
|
||||||
req: HttpRequest,
|
|
||||||
query: web::Path<(String, String, String)>,
|
|
||||||
transcoder: web::Data<Transcoder>,
|
|
||||||
) -> Result<String, ApiError> {
|
|
||||||
let (resource, slug, quality) = query.into_inner();
|
|
||||||
let quality = Quality::from_str(quality.as_str()).map_err(|_| ApiError::BadRequest {
|
|
||||||
error: "Invalid quality".to_string(),
|
|
||||||
})?;
|
|
||||||
let client_id = get_client_id(req)?;
|
|
||||||
|
|
||||||
let path = paths::get_path(resource, slug)
|
|
||||||
.await
|
|
||||||
.map_err(|_| ApiError::NotFound)?;
|
|
||||||
// TODO: Handle start_time that is not 0
|
|
||||||
transcoder
|
|
||||||
.transcode(client_id, path, quality, 0)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
eprintln!("Unhandled error occured while transcoding: {}", e);
|
|
||||||
ApiError::InternalError
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get transmuxed chunk
|
|
||||||
///
|
|
||||||
/// Retrieve a chunk of a transmuxed video.
|
|
||||||
#[utoipa::path(
|
|
||||||
responses(
|
|
||||||
(status = 200, description = "Get a hls chunk."),
|
|
||||||
(status = NOT_FOUND, description = "Invalid slug.")
|
|
||||||
),
|
|
||||||
params(
|
|
||||||
("resource" = String, Path, description = "Episode or movie"),
|
|
||||||
("slug" = String, Path, description = "The slug of the movie/episode."),
|
|
||||||
("quality" = Quality, Path, description = "Specify the quality you want"),
|
|
||||||
("chunk" = u32, Path, description = "The number of the chunk"),
|
|
||||||
("x-client-id" = String, Header, description = "A unique identify for a player's instance. Used to cancel unused transcode"),
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
#[get("/{resource}/{slug}/{quality}/segments-{chunk}.ts")]
|
|
||||||
async fn get_chunk(
|
|
||||||
req: HttpRequest,
|
|
||||||
query: web::Path<(String, String, String, u32)>,
|
|
||||||
transcoder: web::Data<Transcoder>,
|
|
||||||
) -> Result<NamedFile, ApiError> {
|
|
||||||
let (resource, slug, quality, chunk) = query.into_inner();
|
|
||||||
let quality = Quality::from_str(quality.as_str()).map_err(|_| ApiError::BadRequest {
|
|
||||||
error: "Invalid quality".to_string(),
|
|
||||||
})?;
|
|
||||||
let client_id = get_client_id(req)?;
|
|
||||||
|
|
||||||
let path = paths::get_path(resource, slug)
|
|
||||||
.await
|
|
||||||
.map_err(|_| ApiError::NotFound)?;
|
|
||||||
// TODO: Handle start_time that is not 0
|
|
||||||
transcoder
|
|
||||||
.get_segment(client_id, path, quality, chunk)
|
|
||||||
.await
|
|
||||||
.map_err(|_| ApiError::BadRequest {
|
|
||||||
error: "No transcode started for the selected show/quality.".to_string(),
|
|
||||||
})
|
|
||||||
.and_then(|path| {
|
|
||||||
NamedFile::open(path).map_err(|_| ApiError::BadRequest {
|
|
||||||
error: "Invalid segment number.".to_string(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Identify
|
/// Identify
|
||||||
///
|
///
|
||||||
/// Identify metadata about a file
|
/// Identify metadata about a file
|
||||||
@ -194,7 +103,15 @@ async fn get_swagger() -> String {
|
|||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
#[openapi(
|
#[openapi(
|
||||||
info(description = "Transcoder's open api."),
|
info(description = "Transcoder's open api."),
|
||||||
paths(get_direct, get_transcoded, get_master, get_chunk, identify_resource),
|
paths(
|
||||||
|
get_direct,
|
||||||
|
get_master,
|
||||||
|
get_transcoded,
|
||||||
|
get_chunk,
|
||||||
|
get_audio_transcoded,
|
||||||
|
get_audio_chunk,
|
||||||
|
identify_resource
|
||||||
|
),
|
||||||
components(schemas(MediaInfo, Track, Chapter))
|
components(schemas(MediaInfo, Track, Chapter))
|
||||||
)]
|
)]
|
||||||
struct ApiDoc;
|
struct ApiDoc;
|
||||||
@ -210,9 +127,11 @@ async fn main() -> std::io::Result<()> {
|
|||||||
App::new()
|
App::new()
|
||||||
.app_data(state.clone())
|
.app_data(state.clone())
|
||||||
.service(get_direct)
|
.service(get_direct)
|
||||||
.service(get_transcoded)
|
|
||||||
.service(get_master)
|
.service(get_master)
|
||||||
|
.service(get_transcoded)
|
||||||
.service(get_chunk)
|
.service(get_chunk)
|
||||||
|
.service(get_audio_transcoded)
|
||||||
|
.service(get_audio_chunk)
|
||||||
.service(identify_resource)
|
.service(identify_resource)
|
||||||
.service(get_swagger)
|
.service(get_swagger)
|
||||||
})
|
})
|
||||||
@ -220,4 +139,3 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
use actix_web::HttpRequest;
|
||||||
use tokio::{io, process::Child};
|
use tokio::{io, process::Child};
|
||||||
|
|
||||||
|
use crate::error::ApiError;
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
fn kill(pid: i32, sig: i32) -> i32;
|
fn kill(pid: i32, sig: i32) -> i32;
|
||||||
}
|
}
|
||||||
@ -38,3 +41,10 @@ impl Signalable for Child {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_client_id(req: HttpRequest) -> Result<String, ApiError> {
|
||||||
|
req.headers().get("x-client-id")
|
||||||
|
.ok_or(ApiError::BadRequest { error: String::from("Missing client id. Please specify the X-CLIENT-ID header to a guid constant for the lifetime of the player (but unique per instance)."), })
|
||||||
|
.map(|x| x.to_str().unwrap().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
96
transcoder/src/video.rs
Normal file
96
transcoder/src/video.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::ApiError,
|
||||||
|
transcode::{Quality, Transcoder},
|
||||||
|
utils::get_client_id, paths,
|
||||||
|
};
|
||||||
|
use actix_files::NamedFile;
|
||||||
|
use actix_web::{get, web, HttpRequest, Result};
|
||||||
|
|
||||||
|
/// Transcode video
|
||||||
|
///
|
||||||
|
/// Transcode the video to the selected quality.
|
||||||
|
/// This route can take a few seconds to respond since it will way for at least one segment to be
|
||||||
|
/// available.
|
||||||
|
#[utoipa::path(
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get the m3u8 playlist."),
|
||||||
|
(status = NOT_FOUND, description = "Invalid slug.")
|
||||||
|
),
|
||||||
|
params(
|
||||||
|
("resource" = String, Path, description = "Episode or movie"),
|
||||||
|
("slug" = String, Path, description = "The slug of the movie/episode."),
|
||||||
|
("quality" = Quality, Path, description = "Specify the quality you want"),
|
||||||
|
("x-client-id" = String, Header, description = "A unique identify for a player's instance. Used to cancel unused transcode"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#[get("/{resource}/{slug}/{quality}/index.m3u8")]
|
||||||
|
async fn get_transcoded(
|
||||||
|
req: HttpRequest,
|
||||||
|
query: web::Path<(String, String, String)>,
|
||||||
|
transcoder: web::Data<Transcoder>,
|
||||||
|
) -> Result<String, ApiError> {
|
||||||
|
let (resource, slug, quality) = query.into_inner();
|
||||||
|
let quality = Quality::from_str(quality.as_str()).map_err(|_| ApiError::BadRequest {
|
||||||
|
error: "Invalid quality".to_string(),
|
||||||
|
})?;
|
||||||
|
let client_id = get_client_id(req)?;
|
||||||
|
|
||||||
|
let path = paths::get_path(resource, slug)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::NotFound)?;
|
||||||
|
// TODO: Handle start_time that is not 0
|
||||||
|
transcoder
|
||||||
|
.transcode(client_id, path, quality, 0)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Unhandled error occured while transcoding: {}", e);
|
||||||
|
ApiError::InternalError
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get transmuxed chunk
|
||||||
|
///
|
||||||
|
/// Retrieve a chunk of a transmuxed video.
|
||||||
|
#[utoipa::path(
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get a hls chunk."),
|
||||||
|
(status = NOT_FOUND, description = "Invalid slug.")
|
||||||
|
),
|
||||||
|
params(
|
||||||
|
("resource" = String, Path, description = "Episode or movie"),
|
||||||
|
("slug" = String, Path, description = "The slug of the movie/episode."),
|
||||||
|
("quality" = Quality, Path, description = "Specify the quality you want"),
|
||||||
|
("chunk" = u32, Path, description = "The number of the chunk"),
|
||||||
|
("x-client-id" = String, Header, description = "A unique identify for a player's instance. Used to cancel unused transcode"),
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#[get("/{resource}/{slug}/{quality}/segments-{chunk}.ts")]
|
||||||
|
async fn get_chunk(
|
||||||
|
req: HttpRequest,
|
||||||
|
query: web::Path<(String, String, String, u32)>,
|
||||||
|
transcoder: web::Data<Transcoder>,
|
||||||
|
) -> Result<NamedFile, ApiError> {
|
||||||
|
let (resource, slug, quality, chunk) = query.into_inner();
|
||||||
|
let quality = Quality::from_str(quality.as_str()).map_err(|_| ApiError::BadRequest {
|
||||||
|
error: "Invalid quality".to_string(),
|
||||||
|
})?;
|
||||||
|
let client_id = get_client_id(req)?;
|
||||||
|
|
||||||
|
let path = paths::get_path(resource, slug)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::NotFound)?;
|
||||||
|
// TODO: Handle start_time that is not 0
|
||||||
|
transcoder
|
||||||
|
.get_segment(client_id, path, quality, chunk)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::BadRequest {
|
||||||
|
error: "No transcode started for the selected show/quality.".to_string(),
|
||||||
|
})
|
||||||
|
.and_then(|path| {
|
||||||
|
NamedFile::open(path).map_err(|_| ApiError::BadRequest {
|
||||||
|
error: "Invalid segment number.".to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user