1pub(crate) mod download_queue;
20pub(crate) mod session;
21
22use std::{collections::HashMap, fmt::Display};
23
24use futures::AsyncWrite;
25use http::StatusCode;
26use isahc::AsyncReadResponseExt;
27use serde::{Deserialize, Serialize};
28use serde_plain::derive_display_from_serialize;
29use uuid::Uuid;
30
31use crate::{
32 error,
33 isahc_compat::StatusCodeExt,
34 media_container::server::library::{
35 AudioCodec, ContainerFormat, Decision, Protocol, SubtitleCodec, VideoCodec,
36 },
37 url::SERVER_TRANSCODE_ART,
38 HttpClient, Result,
39};
40
41use super::Query;
42
43pub use download_queue::{DownloadQueue, QueueItem, QueueItemStatus};
44pub use session::{TranscodeSession, TranscodeStatus};
45
46#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq)]
47#[serde(rename_all = "lowercase")]
48pub enum Context {
49 Streaming,
50 Static,
51 #[cfg(not(feature = "tests_deny_unknown_fields"))]
52 #[serde(other)]
53 Unknown,
54}
55
56derive_display_from_serialize!(Context);
57
58#[derive(Debug, Clone, Deserialize)]
59#[allow(dead_code)]
60#[serde(rename_all = "camelCase")]
61#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
62struct DecisionResult {
63 available_bandwidth: Option<u32>,
64
65 mde_decision_code: Option<u32>,
66 mde_decision_text: Option<String>,
67
68 general_decision_code: Option<u32>,
69 general_decision_text: Option<String>,
70
71 direct_play_decision_code: Option<u32>,
72 direct_play_decision_text: Option<String>,
73
74 transcode_decision_code: Option<u32>,
75 transcode_decision_text: Option<String>,
76}
77
78#[derive(Debug, Clone, Deserialize)]
79#[serde(rename_all = "camelCase")]
80#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
81pub struct TranscodeSessionStats {
82 pub key: String,
83 pub throttled: bool,
84 pub complete: bool,
85 pub progress: f32,
87 pub size: i64,
88 pub speed: Option<f32>,
89 pub error: bool,
90 pub duration: Option<u64>,
91 pub remaining: Option<u32>,
93 pub context: Context,
94 pub source_video_codec: Option<VideoCodec>,
95 pub source_audio_codec: Option<AudioCodec>,
96 pub video_decision: Option<Decision>,
97 pub audio_decision: Option<Decision>,
98 pub subtitle_decision: Option<Decision>,
99 pub protocol: Protocol,
100 pub container: ContainerFormat,
101 pub video_codec: Option<VideoCodec>,
102 pub audio_codec: Option<AudioCodec>,
103 pub audio_channels: u8,
104 pub width: Option<u32>,
105 pub height: Option<u32>,
106 pub transcode_hw_requested: bool,
107 pub transcode_hw_decoding: Option<String>,
108 pub transcode_hw_encoding: Option<String>,
109 pub transcode_hw_decoding_title: Option<String>,
110 pub transcode_hw_full_pipeline: Option<bool>,
111 pub transcode_hw_encoding_title: Option<String>,
112 #[serde(default)]
113 pub offline_transcode: bool,
114 pub time_stamp: Option<f32>,
115 pub min_offset_available: Option<f32>,
116 pub max_offset_available: Option<f32>,
117}
118
119struct ProfileSetting {
120 setting: String,
121 params: Vec<String>,
122}
123
124impl ProfileSetting {
125 fn new(setting: &str) -> Self {
126 Self {
127 setting: setting.to_owned(),
128 params: Vec::new(),
129 }
130 }
131
132 fn param<N: Display, V: Display>(mut self, name: N, value: V) -> Self {
133 self.params.push(format!("{name}={value}"));
134 self
135 }
136}
137
138impl Display for ProfileSetting {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 write!(f, "{}({})", self.setting, self.params.join("&"))
141 }
142}
143
144#[derive(Debug, Copy, Clone)]
145pub enum VideoSetting {
146 Width,
148 Height,
150 BitDepth,
152 Level,
154 Profile,
156 FrameRate,
158}
159
160impl Display for VideoSetting {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 write!(
163 f,
164 "{}",
165 match self {
166 VideoSetting::Width => "video.width",
167 VideoSetting::Height => "video.height",
168 VideoSetting::BitDepth => "video.bitDepth",
169 VideoSetting::Level => "video.level",
170 VideoSetting::Profile => "video.profile",
171 VideoSetting::FrameRate => "video.frameRate",
172 }
173 )
174 }
175}
176
177#[derive(Debug, Copy, Clone)]
178pub enum AudioSetting {
179 Channels,
181 SamplingRate,
183 BitDepth,
185}
186
187impl Display for AudioSetting {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 write!(
190 f,
191 "{}",
192 match self {
193 AudioSetting::Channels => "audio.channels",
194 AudioSetting::SamplingRate => "audio.samplingRate",
195 AudioSetting::BitDepth => "audio.bitDepth",
196 }
197 )
198 }
199}
200
201#[derive(Debug, Clone)]
202pub enum Constraint {
203 Max(String),
204 Min(String),
205 Match(String),
206 MatchList(Vec<String>),
207 NotMatch(String),
208}
209
210#[derive(Debug, Clone)]
217pub struct Limitation<C, S> {
218 pub codec: Option<C>,
219 pub setting: S,
220 pub constraint: Constraint,
221}
222
223impl<C: ToString, S: ToString> Limitation<C, S> {
224 fn build(&self, scope: &str) -> ProfileSetting {
225 let scope_name = if let Some(codec) = &self.codec {
226 codec.to_string()
227 } else {
228 "*".to_string()
229 };
230 let name = self.setting.to_string();
231
232 let setting = ProfileSetting::new("add-limitation")
233 .param("scope", scope)
234 .param("scopeName", scope_name)
235 .param("name", name);
236
237 match &self.constraint {
238 Constraint::Max(v) => setting.param("type", "upperBound").param("value", v),
239 Constraint::Min(v) => setting.param("type", "lowerBound").param("value", v),
240 Constraint::Match(v) => setting.param("type", "match").param("value", v),
241 Constraint::MatchList(l) => setting.param("type", "match").param(
242 "list",
243 l.iter()
244 .map(|s| s.to_string())
245 .collect::<Vec<String>>()
246 .join("|"),
247 ),
248 Constraint::NotMatch(v) => setting.param("type", "notMatch").param("value", v),
249 }
250 }
251}
252
253impl<C, S> From<(S, Constraint)> for Limitation<C, S> {
254 fn from((setting, constraint): (S, Constraint)) -> Self {
255 Self {
256 codec: None,
257 setting,
258 constraint,
259 }
260 }
261}
262
263impl<C, S> From<(C, S, Constraint)> for Limitation<C, S> {
264 fn from((codec, setting, constraint): (C, S, Constraint)) -> Self {
265 Self {
266 codec: Some(codec),
267 setting,
268 constraint,
269 }
270 }
271}
272
273impl<C, S> From<(Option<C>, S, Constraint)> for Limitation<C, S> {
274 fn from((codec, setting, constraint): (Option<C>, S, Constraint)) -> Self {
275 Self {
276 codec,
277 setting,
278 constraint,
279 }
280 }
281}
282
283pub trait TranscodeOptions {
284 fn transcode_parameters(
285 &self,
286 context: Context,
287 protocol: Protocol,
288 container: Option<ContainerFormat>,
289 ) -> HashMap<String, String>;
290}
291
292#[derive(Debug, Clone)]
307pub struct VideoTranscodeOptions {
308 pub bitrate: u32,
313 pub width: u32,
315 pub height: u32,
317 pub video_quality: Option<u32>,
319 pub audio_boost: Option<u8>,
321 pub burn_subtitles: bool,
323 pub containers: Vec<ContainerFormat>,
325 pub video_codecs: Vec<VideoCodec>,
327 pub video_limitations: Vec<Limitation<VideoCodec, VideoSetting>>,
329 pub audio_codecs: Vec<AudioCodec>,
331 pub audio_limitations: Vec<Limitation<AudioCodec, AudioSetting>>,
333 pub subtitle_codecs: Vec<SubtitleCodec>,
335}
336
337impl Default for VideoTranscodeOptions {
338 fn default() -> Self {
339 Self {
340 bitrate: 4000,
341 width: 1280,
342 height: 720,
343 video_quality: None,
344 audio_boost: None,
345 burn_subtitles: false,
346 containers: vec![ContainerFormat::Mp4, ContainerFormat::Mkv],
347 video_codecs: vec![VideoCodec::H264],
348 video_limitations: Default::default(),
349 audio_codecs: vec![AudioCodec::Aac, AudioCodec::Mp3],
350 audio_limitations: Default::default(),
351 subtitle_codecs: Default::default(),
352 }
353 }
354}
355
356impl TranscodeOptions for VideoTranscodeOptions {
357 fn transcode_parameters(
358 &self,
359 context: Context,
360 protocol: Protocol,
361 container: Option<ContainerFormat>,
362 ) -> HashMap<String, String> {
363 let mut query = Query::new()
364 .param("maxVideoBitrate", self.bitrate.to_string())
365 .param("videoBitrate", self.bitrate.to_string())
366 .param("videoResolution", format!("{}x{}", self.width, self.height))
367 .param("transcodeType", "video");
368
369 if self.burn_subtitles {
370 query = query
371 .param("subtitles", "burn")
372 .param("subtitleSize", "100");
373 } else {
374 query = query
375 .param("subtitles", "auto")
376 .param("subtitleSize", "100");
377 }
378
379 if let Some(boost) = self.audio_boost {
380 query = query.param("audioBoost", boost.to_string());
381 }
382
383 if let Some(q) = self.video_quality {
384 query = query.param("videoQuality", q.clamp(0, 99).to_string());
385 }
386
387 let video_codecs = self
388 .video_codecs
389 .iter()
390 .map(ToString::to_string)
391 .collect::<Vec<String>>()
392 .join(",");
393
394 let audio_codecs = self
395 .audio_codecs
396 .iter()
397 .map(ToString::to_string)
398 .collect::<Vec<String>>()
399 .join(",");
400
401 let subtitle_codecs = self
402 .subtitle_codecs
403 .iter()
404 .map(ToString::to_string)
405 .collect::<Vec<String>>()
406 .join(",");
407
408 let containers = if let Some(container) = container {
409 vec![container.to_string()]
410 } else {
411 self.containers.iter().map(ToString::to_string).collect()
412 };
413
414 let mut profile = Vec::new();
415
416 let mut is_first = true;
417 for container in &containers {
418 let mut setting = ProfileSetting::new("add-transcode-target")
419 .param("type", "videoProfile")
420 .param("context", context.to_string())
421 .param("protocol", protocol.to_string())
422 .param("container", container)
423 .param("videoCodec", &video_codecs)
424 .param("audioCodec", &audio_codecs)
425 .param("subtitleCodec", &subtitle_codecs);
426
427 if is_first {
428 is_first = false;
429 setting = setting.param("replace", "true");
431 }
432
433 profile.push(setting.to_string());
434 }
435
436 if context == Context::Static {
438 profile.push(
439 ProfileSetting::new("add-direct-play-profile")
440 .param("type", "videoProfile")
441 .param("container", containers.join(","))
442 .param("videoCodec", &video_codecs)
443 .param("audioCodec", &audio_codecs)
444 .param("subtitleCodec", &subtitle_codecs)
445 .param("replace", "true")
446 .to_string(),
447 );
448 }
449
450 profile.extend(
451 self.video_limitations
452 .iter()
453 .map(|l| l.build("videoCodec").to_string()),
454 );
455 profile.extend(
456 self.audio_limitations
457 .iter()
458 .map(|l| l.build("videoAudioCodec").to_string()),
459 );
460
461 query
462 .param("X-Plex-Client-Profile-Extra", profile.join("+"))
463 .into()
464 }
465}
466
467#[derive(Debug, Clone)]
482pub struct MusicTranscodeOptions {
483 pub bitrate: u32,
485 pub containers: Vec<ContainerFormat>,
487 pub codecs: Vec<AudioCodec>,
489 pub limitations: Vec<Limitation<AudioCodec, AudioSetting>>,
491}
492
493impl Default for MusicTranscodeOptions {
494 fn default() -> Self {
495 Self {
496 bitrate: 192,
497 containers: vec![ContainerFormat::Mp3],
498 codecs: vec![AudioCodec::Mp3],
499 limitations: Default::default(),
500 }
501 }
502}
503
504impl TranscodeOptions for MusicTranscodeOptions {
505 fn transcode_parameters(
506 &self,
507 context: Context,
508 protocol: Protocol,
509 container: Option<ContainerFormat>,
510 ) -> HashMap<String, String> {
511 let query = Query::new()
512 .param("musicBitrate", self.bitrate.to_string())
513 .param("transcodeType", "music");
514
515 let audio_codecs = self
516 .codecs
517 .iter()
518 .map(|c| c.to_string())
519 .collect::<Vec<String>>()
520 .join(",");
521
522 let containers = if let Some(container) = container {
523 vec![container.to_string()]
524 } else {
525 self.containers.iter().map(ToString::to_string).collect()
526 };
527
528 let mut profile = Vec::new();
529
530 let mut is_first = true;
531 for container in &containers {
532 let mut setting = ProfileSetting::new("add-transcode-target")
533 .param("type", "musicProfile")
534 .param("context", context.to_string())
535 .param("protocol", protocol.to_string())
536 .param("container", container)
537 .param("audioCodec", &audio_codecs);
538
539 if is_first {
540 is_first = false;
541 setting = setting.param("replace", "true");
543 }
544
545 profile.push(setting.to_string());
546 }
547
548 if context == Context::Static {
550 profile.push(
551 ProfileSetting::new("add-direct-play-profile")
552 .param("type", "musicProfile")
553 .param("container", containers.join(","))
554 .param("audioCodec", &audio_codecs)
555 .to_string(),
556 );
557 }
558
559 profile.extend(
560 self.limitations
561 .iter()
562 .map(|l| l.build("audioCodec").to_string()),
563 );
564
565 query
566 .param("X-Plex-Client-Profile-Extra", profile.join("+"))
567 .into()
568 }
569}
570
571fn session_id() -> String {
573 Uuid::new_v4().as_simple().to_string()
574}
575
576fn bs(val: bool) -> String {
577 if val {
578 "1".to_string()
579 } else {
580 "0".to_string()
581 }
582}
583
584fn get_transcode_params<O: TranscodeOptions>(
585 id: &str,
586 context: Context,
587 protocol: Protocol,
588 media_index: Option<usize>,
589 part_index: Option<usize>,
590 options: O,
591) -> Result<Query> {
592 let container = match (context, protocol) {
593 (Context::Static, _) => None,
594 (_, Protocol::Dash) => Some(ContainerFormat::Mp4),
595 (_, Protocol::Hls) => Some(ContainerFormat::MpegTs),
596 _ => return Err(error::Error::InvalidTranscodeSettings),
597 };
598
599 let mut query = Query::new()
600 .param("session", id)
603 .param("transcodeSessionId", id)
604 .param("directPlay", bs(context == Context::Static))
608 .param("directStream", bs(true))
610 .param("directStreamAudio", bs(true))
612 .param("protocol", protocol.to_string())
613 .param("context", context.to_string())
614 .param("location", "lan")
615 .param("fastSeek", bs(true));
616
617 if let Some(index) = media_index {
618 query = query.param("mediaIndex", index.to_string());
619 } else {
620 query = query.param("mediaIndex", "-1");
621 }
622
623 if let Some(index) = part_index {
624 query = query.param("partIndex", index.to_string());
625 } else {
626 query = query.param("partIndex", "-1");
627 }
628
629 Ok(query.append(options.transcode_parameters(context, protocol, container)))
630}
631
632#[derive(Debug, Clone, Copy)]
633pub struct ArtTranscodeOptions {
634 pub upscale: bool,
637 pub min_size: bool,
640}
641
642impl Default for ArtTranscodeOptions {
643 fn default() -> Self {
644 Self {
645 upscale: true,
646 min_size: true,
647 }
648 }
649}
650
651pub(crate) async fn transcode_artwork<W>(
652 client: &HttpClient,
653 art: &str,
654 width: u32,
655 height: u32,
656 options: ArtTranscodeOptions,
657 writer: W,
658) -> Result<()>
659where
660 W: AsyncWrite + Unpin,
661{
662 let query = Query::new()
663 .param("url", art)
664 .param("upscale", bs(options.upscale))
665 .param("minSize", bs(options.min_size))
666 .param("width", width.to_string())
667 .param("height", height.to_string());
668
669 let mut response = client
670 .get(format!("{SERVER_TRANSCODE_ART}?{query}"))
671 .send()
672 .await?;
673
674 match response.status().as_http_status() {
675 StatusCode::OK => {
678 response.copy_to(writer).await?;
679 Ok(())
680 }
681 _ => Err(crate::Error::from_response(response).await),
682 }
683}