pub(crate) mod download_queue;
pub(crate) mod session;
use std::{collections::HashMap, fmt::Display};
use futures::AsyncWrite;
use http::StatusCode;
use isahc::AsyncReadResponseExt;
use serde::{Deserialize, Serialize};
use serde_plain::derive_display_from_serialize;
use uuid::Uuid;
use crate::{
error,
isahc_compat::StatusCodeExt,
media_container::server::library::{
AudioCodec, ContainerFormat, Decision, Protocol, SubtitleCodec, VideoCodec,
},
url::SERVER_TRANSCODE_ART,
HttpClient, Result,
};
use super::Query;
pub use download_queue::{DownloadQueue, QueueItem, QueueItemStatus};
pub use session::{TranscodeSession, TranscodeStatus};
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Context {
Streaming,
Static,
#[cfg(not(feature = "tests_deny_unknown_fields"))]
#[serde(other)]
Unknown,
}
derive_display_from_serialize!(Context);
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
struct DecisionResult {
available_bandwidth: Option<u32>,
mde_decision_code: Option<u32>,
mde_decision_text: Option<String>,
general_decision_code: Option<u32>,
general_decision_text: Option<String>,
direct_play_decision_code: Option<u32>,
direct_play_decision_text: Option<String>,
transcode_decision_code: Option<u32>,
transcode_decision_text: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
pub struct TranscodeSessionStats {
pub key: String,
pub throttled: bool,
pub complete: bool,
pub progress: f32,
pub size: i64,
pub speed: Option<f32>,
pub error: bool,
pub duration: Option<u64>,
pub remaining: Option<u32>,
pub context: Context,
pub source_video_codec: Option<VideoCodec>,
pub source_audio_codec: Option<AudioCodec>,
pub video_decision: Option<Decision>,
pub audio_decision: Option<Decision>,
pub subtitle_decision: Option<Decision>,
pub protocol: Protocol,
pub container: ContainerFormat,
pub video_codec: Option<VideoCodec>,
pub audio_codec: Option<AudioCodec>,
pub audio_channels: u8,
pub width: Option<u32>,
pub height: Option<u32>,
pub transcode_hw_requested: bool,
pub transcode_hw_decoding: Option<String>,
pub transcode_hw_encoding: Option<String>,
pub transcode_hw_decoding_title: Option<String>,
pub transcode_hw_full_pipeline: Option<bool>,
pub transcode_hw_encoding_title: Option<String>,
#[serde(default)]
pub offline_transcode: bool,
pub time_stamp: Option<f32>,
pub min_offset_available: Option<f32>,
pub max_offset_available: Option<f32>,
}
struct ProfileSetting {
setting: String,
params: Vec<String>,
}
impl ProfileSetting {
fn new(setting: &str) -> Self {
Self {
setting: setting.to_owned(),
params: Vec::new(),
}
}
fn param<N: Display, V: Display>(mut self, name: N, value: V) -> Self {
self.params.push(format!("{name}={value}"));
self
}
}
impl Display for ProfileSetting {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}({})", self.setting, self.params.join("&"))
}
}
#[derive(Debug, Copy, Clone)]
pub enum VideoSetting {
Width,
Height,
BitDepth,
Level,
Profile,
FrameRate,
}
impl Display for VideoSetting {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
VideoSetting::Width => "video.width",
VideoSetting::Height => "video.height",
VideoSetting::BitDepth => "video.bitDepth",
VideoSetting::Level => "video.level",
VideoSetting::Profile => "video.profile",
VideoSetting::FrameRate => "video.frameRate",
}
)
}
}
#[derive(Debug, Copy, Clone)]
pub enum AudioSetting {
Channels,
SamplingRate,
BitDepth,
}
impl Display for AudioSetting {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
AudioSetting::Channels => "audio.channels",
AudioSetting::SamplingRate => "audio.samplingRate",
AudioSetting::BitDepth => "audio.bitDepth",
}
)
}
}
#[derive(Debug, Clone)]
pub enum Constraint {
Max(String),
Min(String),
Match(String),
MatchList(Vec<String>),
NotMatch(String),
}
#[derive(Debug, Clone)]
pub struct Limitation<C, S> {
pub codec: Option<C>,
pub setting: S,
pub constraint: Constraint,
}
impl<C: ToString, S: ToString> Limitation<C, S> {
fn build(&self, scope: &str) -> ProfileSetting {
let scope_name = if let Some(codec) = &self.codec {
codec.to_string()
} else {
"*".to_string()
};
let name = self.setting.to_string();
let setting = ProfileSetting::new("add-limitation")
.param("scope", scope)
.param("scopeName", scope_name)
.param("name", name);
match &self.constraint {
Constraint::Max(v) => setting.param("type", "upperBound").param("value", v),
Constraint::Min(v) => setting.param("type", "lowerBound").param("value", v),
Constraint::Match(v) => setting.param("type", "match").param("value", v),
Constraint::MatchList(l) => setting.param("type", "match").param(
"list",
l.iter()
.map(|s| s.to_string())
.collect::<Vec<String>>()
.join("|"),
),
Constraint::NotMatch(v) => setting.param("type", "notMatch").param("value", v),
}
}
}
impl<C, S> From<(S, Constraint)> for Limitation<C, S> {
fn from((setting, constraint): (S, Constraint)) -> Self {
Self {
codec: None,
setting,
constraint,
}
}
}
impl<C, S> From<(C, S, Constraint)> for Limitation<C, S> {
fn from((codec, setting, constraint): (C, S, Constraint)) -> Self {
Self {
codec: Some(codec),
setting,
constraint,
}
}
}
impl<C, S> From<(Option<C>, S, Constraint)> for Limitation<C, S> {
fn from((codec, setting, constraint): (Option<C>, S, Constraint)) -> Self {
Self {
codec,
setting,
constraint,
}
}
}
pub trait TranscodeOptions {
fn transcode_parameters(
&self,
context: Context,
protocol: Protocol,
container: Option<ContainerFormat>,
) -> HashMap<String, String>;
}
#[derive(Debug, Clone)]
pub struct VideoTranscodeOptions {
pub bitrate: u32,
pub width: u32,
pub height: u32,
pub video_quality: Option<u32>,
pub audio_boost: Option<u8>,
pub burn_subtitles: bool,
pub containers: Vec<ContainerFormat>,
pub video_codecs: Vec<VideoCodec>,
pub video_limitations: Vec<Limitation<VideoCodec, VideoSetting>>,
pub audio_codecs: Vec<AudioCodec>,
pub audio_limitations: Vec<Limitation<AudioCodec, AudioSetting>>,
pub subtitle_codecs: Vec<SubtitleCodec>,
}
impl Default for VideoTranscodeOptions {
fn default() -> Self {
Self {
bitrate: 4000,
width: 1280,
height: 720,
video_quality: None,
audio_boost: None,
burn_subtitles: false,
containers: vec![ContainerFormat::Mp4, ContainerFormat::Mkv],
video_codecs: vec![VideoCodec::H264],
video_limitations: Default::default(),
audio_codecs: vec![AudioCodec::Aac, AudioCodec::Mp3],
audio_limitations: Default::default(),
subtitle_codecs: Default::default(),
}
}
}
impl TranscodeOptions for VideoTranscodeOptions {
fn transcode_parameters(
&self,
context: Context,
protocol: Protocol,
container: Option<ContainerFormat>,
) -> HashMap<String, String> {
let mut query = Query::new()
.param("maxVideoBitrate", self.bitrate.to_string())
.param("videoBitrate", self.bitrate.to_string())
.param("videoResolution", format!("{}x{}", self.width, self.height))
.param("transcodeType", "video");
if self.burn_subtitles {
query = query
.param("subtitles", "burn")
.param("subtitleSize", "100");
} else {
query = query
.param("subtitles", "auto")
.param("subtitleSize", "100");
}
if let Some(boost) = self.audio_boost {
query = query.param("audioBoost", boost.to_string());
}
if let Some(q) = self.video_quality {
query = query.param("videoQuality", q.clamp(0, 99).to_string());
}
let video_codecs = self
.video_codecs
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.join(",");
let audio_codecs = self
.audio_codecs
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.join(",");
let subtitle_codecs = self
.subtitle_codecs
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.join(",");
let containers = if let Some(container) = container {
vec![container.to_string()]
} else {
self.containers.iter().map(ToString::to_string).collect()
};
let mut profile = Vec::new();
let mut is_first = true;
for container in &containers {
let mut setting = ProfileSetting::new("add-transcode-target")
.param("type", "videoProfile")
.param("context", context.to_string())
.param("protocol", protocol.to_string())
.param("container", container)
.param("videoCodec", &video_codecs)
.param("audioCodec", &audio_codecs)
.param("subtitleCodec", &subtitle_codecs);
if is_first {
is_first = false;
setting = setting.param("replace", "true");
}
profile.push(setting.to_string());
}
if context == Context::Static {
profile.push(
ProfileSetting::new("add-direct-play-profile")
.param("type", "videoProfile")
.param("container", containers.join(","))
.param("videoCodec", &video_codecs)
.param("audioCodec", &audio_codecs)
.param("subtitleCodec", &subtitle_codecs)
.param("replace", "true")
.to_string(),
);
}
profile.extend(
self.video_limitations
.iter()
.map(|l| l.build("videoCodec").to_string()),
);
profile.extend(
self.audio_limitations
.iter()
.map(|l| l.build("videoAudioCodec").to_string()),
);
query
.param("X-Plex-Client-Profile-Extra", profile.join("+"))
.into()
}
}
#[derive(Debug, Clone)]
pub struct MusicTranscodeOptions {
pub bitrate: u32,
pub containers: Vec<ContainerFormat>,
pub codecs: Vec<AudioCodec>,
pub limitations: Vec<Limitation<AudioCodec, AudioSetting>>,
}
impl Default for MusicTranscodeOptions {
fn default() -> Self {
Self {
bitrate: 192,
containers: vec![ContainerFormat::Mp3],
codecs: vec![AudioCodec::Mp3],
limitations: Default::default(),
}
}
}
impl TranscodeOptions for MusicTranscodeOptions {
fn transcode_parameters(
&self,
context: Context,
protocol: Protocol,
container: Option<ContainerFormat>,
) -> HashMap<String, String> {
let query = Query::new()
.param("musicBitrate", self.bitrate.to_string())
.param("transcodeType", "music");
let audio_codecs = self
.codecs
.iter()
.map(|c| c.to_string())
.collect::<Vec<String>>()
.join(",");
let containers = if let Some(container) = container {
vec![container.to_string()]
} else {
self.containers.iter().map(ToString::to_string).collect()
};
let mut profile = Vec::new();
let mut is_first = true;
for container in &containers {
let mut setting = ProfileSetting::new("add-transcode-target")
.param("type", "musicProfile")
.param("context", context.to_string())
.param("protocol", protocol.to_string())
.param("container", container)
.param("audioCodec", &audio_codecs);
if is_first {
is_first = false;
setting = setting.param("replace", "true");
}
profile.push(setting.to_string());
}
if context == Context::Static {
profile.push(
ProfileSetting::new("add-direct-play-profile")
.param("type", "musicProfile")
.param("container", containers.join(","))
.param("audioCodec", &audio_codecs)
.to_string(),
);
}
profile.extend(
self.limitations
.iter()
.map(|l| l.build("audioCodec").to_string()),
);
query
.param("X-Plex-Client-Profile-Extra", profile.join("+"))
.into()
}
}
fn session_id() -> String {
Uuid::new_v4().as_simple().to_string()
}
fn bs(val: bool) -> String {
if val {
"1".to_string()
} else {
"0".to_string()
}
}
fn get_transcode_params<O: TranscodeOptions>(
id: &str,
context: Context,
protocol: Protocol,
media_index: Option<usize>,
part_index: Option<usize>,
options: O,
) -> Result<Query> {
let container = match (context, protocol) {
(Context::Static, _) => None,
(_, Protocol::Dash) => Some(ContainerFormat::Mp4),
(_, Protocol::Hls) => Some(ContainerFormat::MpegTs),
_ => return Err(error::Error::InvalidTranscodeSettings),
};
let mut query = Query::new()
.param("session", id)
.param("transcodeSessionId", id)
.param("directPlay", bs(context == Context::Static))
.param("directStream", bs(true))
.param("directStreamAudio", bs(true))
.param("protocol", protocol.to_string())
.param("context", context.to_string())
.param("location", "lan")
.param("fastSeek", bs(true));
if let Some(index) = media_index {
query = query.param("mediaIndex", index.to_string());
} else {
query = query.param("mediaIndex", "-1");
}
if let Some(index) = part_index {
query = query.param("partIndex", index.to_string());
} else {
query = query.param("partIndex", "-1");
}
Ok(query.append(options.transcode_parameters(context, protocol, container)))
}
#[derive(Debug, Clone, Copy)]
pub struct ArtTranscodeOptions {
pub upscale: bool,
pub min_size: bool,
}
impl Default for ArtTranscodeOptions {
fn default() -> Self {
Self {
upscale: true,
min_size: true,
}
}
}
pub(crate) async fn transcode_artwork<W>(
client: &HttpClient,
art: &str,
width: u32,
height: u32,
options: ArtTranscodeOptions,
writer: W,
) -> Result<()>
where
W: AsyncWrite + Unpin,
{
let query = Query::new()
.param("url", art)
.param("upscale", bs(options.upscale))
.param("minSize", bs(options.min_size))
.param("width", width.to_string())
.param("height", height.to_string());
let mut response = client
.get(format!("{SERVER_TRANSCODE_ART}?{query}"))
.send()
.await?;
match response.status().as_http_status() {
StatusCode::OK => {
response.copy_to(writer).await?;
Ok(())
}
_ => Err(crate::Error::from_response(response).await),
}
}