You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

278 lines
8.2 KiB
Rust

#![allow(dead_code)]
use crate::av::init;
use crate::transcoder_manager::{TranscoderInstance, TranscoderManager};
use askama::Template;
use async_std::fs::File;
use async_std::sync::{Arc, RwLock};
use ffmpeg_sys_next::{av_make_error_string, av_malloc, av_version_info};
use http_types::Error as HttpError;
use lazy_static::lazy_static;
use serde::Serialize;
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::ops::Deref;
use std::option::Option::Some;
use std::os::raw::{c_char, c_int};
use std::str::FromStr;
use tide::{Body, Request, Response, StatusCode};
use uuid::Uuid;
pub mod av;
pub mod transcoder;
pub mod transcoder_manager;
pub mod utils;
const OUTPUT_PATH: &str = "/tmp/transotf";
fn av_err2str(error_code: c_int) -> String {
unsafe {
let str: *mut c_char = av_malloc(1024).cast();
av_make_error_string(str, 1024, error_code);
CString::from_raw(str).to_str().unwrap().to_string()
}
}
lazy_static! {
static ref HOME_PAGE: String = {
format!(
"{}: {}\nffmpeg: {}",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
unsafe { CStr::from_ptr(av_version_info()).to_str().unwrap_or("n/a") },
)
};
static ref FAVICON: Vec<u8> = include_bytes!("../resources/favicon.ico").to_vec();
}
#[derive(Default, Clone)]
struct State {
manager: Arc<RwLock<TranscoderManager>>,
}
#[derive(Serialize)]
struct CreateDTO {
id: String,
manifest: String,
_player: String,
}
fn build_url(req: &Request<State>, url: String) -> String {
let host = req.header("host").and_then(|x| x.get(0)).unwrap().clone();
format!("http://{}/{}", host, url.trim_start_matches("/"))
}
async fn create_transcode(req: Request<State>) -> Result<Response, HttpError> {
let params = req.query::<HashMap<String, String>>()?;
let target = params.get(&"target".to_string());
let target = if let Some(target) = target {
target
} else {
return Err(HttpError::from_str(
StatusCode::BadRequest,
"no target given",
));
};
{
File::open(target).await.map_err(|err| {
HttpError::from_str(
StatusCode::BadRequest,
format!("Failed to open file: {}", err),
)
})?;
}
let id = req
.state()
.manager
.write()
.await
.start(target.clone(), None, None)
.map_err(|err| {
HttpError::from_str(
StatusCode::InternalServerError,
format!("Failed to start transcoder: {}", err),
)
})?;
let id = id.to_string();
let dto = CreateDTO {
manifest: build_url(&req, format!("/session/{}/manifest.mpd", id)),
_player: build_url(&req, format!("/session/{}/player", id)),
id,
};
let mut resp = Response::new(200);
resp.set_body(Body::from_json(&dto)?);
resp.insert_header("Content-Type", "application/json");
Ok(resp)
}
async fn get_instance(req: &Request<State>) -> Result<Arc<TranscoderInstance>, HttpError> {
let id = req.param("id")?;
let id = Uuid::from_str(&id)?;
let manager = req.state().manager.read().await;
if let Some(instance) = manager.get(id) {
Ok(instance)
} else {
Err(HttpError::from_str(
StatusCode::NotFound,
format!("Can't find session with id {}", id),
))
}
}
#[derive(Template)]
#[template(path = "manifest.xml")]
struct MPDManifest {
id: Uuid,
has_audio: bool,
duration: f64,
is_vlc: bool,
audio_time_base_den: i32,
video_time_base_den: i32,
}
mod filters {
pub fn iso_duration(secs: &f64) -> ::askama::Result<String> {
let minutes = (secs / 60.0).floor();
let secs = ((secs % 60.0) * 100.0).floor() / 100.0;
let hours = (minutes / 60.0).floor();
let minutes = minutes % 60.0;
return Ok(format!("PT{}H{}M{}S", hours, minutes, secs));
}
}
async fn get_manifest(req: Request<State>) -> Result<Response, HttpError> {
let transcoder_instance = get_instance(&req).await?;
let id = transcoder_instance.id();
let status = transcoder_instance.status().await;
let mut resp = Response::new(200);
resp.insert_header("Content-Type", "application/dash+xml");
let mpd = MPDManifest {
id,
has_audio: status.audio.is_some(),
duration: status.duration_secs,
is_vlc: req
.header("User-Agent")
.map(|x| x.last().to_string())
.map_or(false, |x| x.starts_with("VLC")),
audio_time_base_den: status.audio.map_or(0, |audio| audio.time_scale),
video_time_base_den: status.video.time_scale,
};
resp.set_body(mpd.render().unwrap());
Ok(resp)
}
async fn get_init(req: Request<State>) -> Result<Response, HttpError> {
let transcoder_instance = get_instance(&req).await?;
let type_ = req.param("type").unwrap();
let id = transcoder_instance.id();
if (type_ != "audio" && type_ != "video") || !transcoder_instance.wait_for_init().await {
return Ok(Response::new(StatusCode::NotFound));
}
let mut ok = Response::new(StatusCode::Ok);
ok.insert_header("Content-Type", "video/mp4");
ok.set_body(Body::from_file(format!("{}/{}/{}-init.mp4", OUTPUT_PATH, id, type_)).await?);
Ok(ok)
}
async fn get_segment(req: Request<State>) -> Result<Response, HttpError> {
let transcoder_instance = get_instance(&req).await?;
let type_ = req.param("type").unwrap();
let segment = req.param("nr").unwrap();
let id = transcoder_instance.id();
if !segment.ends_with(".m4s")
|| (type_ != "audio" && type_ != "video")
|| !transcoder_instance.wait_for_init().await
{
return Ok(Response::new(StatusCode::NotFound));
}
let segment = &segment[0..segment.len() - 4];
let segment = if let Ok(segment) = u32::from_str(segment) {
segment
} else {
return Ok(Response::new(StatusCode::NotFound));
};
let status = transcoder_instance.status().await;
if (segment as i64 - 5) > status.current_segment as i64 || segment < status.current_segment {
if !status.segments.contains(&segment) {
transcoder_instance.seek(segment).await;
}
}
if !transcoder_instance.wait_for_segment(segment).await {
return Ok(Response::new(StatusCode::NotFound));
}
let mut ok = Response::new(StatusCode::Ok);
ok.insert_header("Content-Type", "video/mp4");
ok.set_body(
Body::from_file(format!(
"{}/{}/{}-segment-{:0>5}.m4s",
OUTPUT_PATH, id, type_, segment
))
.await?,
);
Ok(ok)
}
async fn get_player(req: Request<State>) -> Result<Response, HttpError> {
get_instance(&req).await?;
let mut resp = Response::new(StatusCode::Ok);
resp.insert_header("Content-Type", "text/html");
resp.set_body(include_str!("../resources/player.html"));
Ok(resp)
}
async fn get_favicon(_: Request<State>) -> Result<Response, HttpError> {
let mut resp = Response::new(StatusCode::Ok);
resp.insert_header("Content-Type", "image/x-icon");
resp.set_body(Vec::deref(&FAVICON));
Ok(resp)
}
async fn get_status(req: Request<State>) -> Result<Response, HttpError> {
let instance = get_instance(&req).await?;
let mut resp = Response::new(StatusCode::Ok);
resp.insert_header("Content-Type", "application/json");
resp.set_body(Body::from_json(&instance.status().await)?);
Ok(resp)
}
#[async_std::main]
async fn main() -> std::io::Result<()> {
init();
let mut app = tide::with_state(State::default());
app.at("/").get(|_| async { Ok(HOME_PAGE.clone()) });
app.at("/favicon.ico").get(get_favicon);
app.at("/transcode").get(create_transcode);
app.at("/session/:id/status.json").get(get_status);
app.at("/session/:id/player").get(get_player);
app.at("/session/:id/manifest").get(get_manifest);
app.at("/session/:id/manifest.xml").get(get_manifest);
app.at("/session/:id/manifest.mpd").get(get_manifest);
app.at("/session/:id/:type/init.mp4").get(get_init);
app.at("/session/:id/:type/:nr").get(get_segment);
let listen_fut = app.listen("0:8000");
println!("Listening on 0:8000");
listen_fut.await
}