use std::collections::HashMap;
use std::path::PathBuf;
use crate::PROTOCOL_VERSION;
use crate::assets::ProxyRequest;
use crate::effect_request::EffectRequest;
use alpha2_assets::MediaPurpose;
use alpha2_media::{BitrateMode, QualityLevel, VideoRateControl};
use alpha2_native::{
NativeAudioCodec, NativeVideoCodec, codec_capabilities, hardware_decode_capabilities,
};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::value::RawValue;
use serde_json::{Value, json};
pub const MAX_REQUEST_LINE_BYTES: usize = 2 * alpha2_model::MAX_PROJECT_JSON_BYTES;
pub const MAX_REQUEST_SETTING_BYTES: usize = 4 * 1024;
pub enum Request {
Capabilities {
protocol_version: u32,
request_id: String,
},
Export(Box<ExportRequest>),
Conform(ConformRequest),
}
#[derive(Deserialize)]
#[serde(transparent)]
pub struct ProjectJson(Box<RawValue>);
impl ProjectJson {
pub fn from_value(project: &Value) -> Result<Self, HostError> {
serde_json::value::to_raw_value(project)
.map(Self)
.map_err(HostError::json)
}
pub fn text(&self) -> &str {
self.0.get()
}
}
#[derive(Deserialize)]
struct CapabilitiesRequest {
#[serde(rename = "protocolVersion")]
protocol_version: u32,
#[serde(rename = "requestId")]
request_id: String,
}
#[derive(Deserialize)]
pub struct ExportRequest {
#[serde(rename = "protocolVersion")]
pub protocol_version: u32,
#[serde(rename = "requestId")]
pub request_id: String,
pub project: ProjectJson,
pub assets: HashMap<String, PathBuf>,
#[serde(default)]
pub proxies: HashMap<String, Vec<ProxyRequest>>,
#[serde(default)]
pub frames: HashMap<String, Vec<Option<PathBuf>>>,
#[serde(default)]
pub resources: HashMap<String, HashMap<String, PathBuf>>,
#[serde(default)]
pub media: ExportMedia,
#[serde(rename = "effectAssets", default)]
pub effect_assets: HashMap<String, PathBuf>,
#[serde(rename = "fonts", default)]
pub font_files: Vec<PathBuf>,
#[serde(default)]
pub effects: Vec<EffectRequest>,
#[serde(rename = "startTicks")]
pub start_ticks: i64,
#[serde(rename = "endTicks")]
pub end_ticks: i64,
#[serde(rename = "frameRate")]
pub frame_rate: RateRequest,
#[serde(rename = "includeAudio", default = "include_audio_by_default")]
pub include_audio: bool,
#[serde(rename = "videoCodec", default)]
pub video_codec: VideoCodecRequest,
#[serde(rename = "videoEncoder", default)]
pub video_encoder: Option<String>,
#[serde(rename = "inputPolicy", default)]
pub input_policy: crate::config::VideoInputPolicy,
#[serde(default)]
pub strategy: crate::config::ExportStrategy,
#[serde(rename = "videoQuality", default)]
pub video_quality: VideoQualityRequest,
#[serde(rename = "audioCodec", default)]
pub audio_codec: AudioCodecRequest,
#[serde(rename = "audioQuality", default)]
pub audio_quality: AudioQualityRequest,
#[serde(default)]
pub loudness: Option<LoudnessRequest>,
#[serde(rename = "renderCache", default, deserialize_with = "render_cache")]
pub render_cache: Option<crate::render_cache::RenderCacheRequest>,
#[serde(default)]
pub memory: crate::memory::MemoryRequest,
pub output: PathBuf,
}
impl ExportRequest {
pub fn check(&self) -> Result<(), HostError> {
if self.loudness.is_some() && !self.include_audio {
return Err(HostError::invalid_input(
"loudness normalizes the export's audio and needs includeAudio: true",
));
}
Ok(())
}
}
#[derive(Deserialize)]
pub struct ConformRequest {
#[serde(rename = "protocolVersion")]
pub protocol_version: u32,
#[serde(rename = "requestId")]
pub request_id: String,
pub project: ProjectJson,
#[serde(default)]
pub assets: HashMap<String, PathBuf>,
#[serde(default)]
pub proxies: HashMap<String, Vec<ProxyRequest>>,
#[serde(default)]
pub frames: HashMap<String, Vec<Option<PathBuf>>>,
#[serde(default)]
pub purpose: PurposeRequest,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum ExportMedia {
#[default]
Original,
Proxies,
}
impl ExportMedia {
pub const fn purpose(self) -> MediaPurpose {
match self {
Self::Original => MediaPurpose::ExportOriginal,
Self::Proxies => MediaPurpose::ExportProxy,
}
}
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum PurposeRequest {
Preview,
PreviewProxy,
Thumbnail,
RemoteEdit,
#[default]
ExportOriginal,
ExportProxy,
}
impl PurposeRequest {
pub const fn purpose(self) -> MediaPurpose {
match self {
Self::Preview => MediaPurpose::Preview,
Self::PreviewProxy => MediaPurpose::PreviewProxy,
Self::Thumbnail => MediaPurpose::Thumbnail,
Self::RemoteEdit => MediaPurpose::RemoteEdit,
Self::ExportOriginal => MediaPurpose::ExportOriginal,
Self::ExportProxy => MediaPurpose::ExportProxy,
}
}
}
impl Request {
pub fn request_id(&self) -> &str {
match self {
Self::Capabilities { request_id, .. } => request_id,
Self::Export(export) => &export.request_id,
Self::Conform(conform) => &conform.request_id,
}
}
pub fn protocol_version(&self) -> u32 {
match self {
Self::Capabilities {
protocol_version, ..
} => *protocol_version,
Self::Export(export) => export.protocol_version,
Self::Conform(conform) => conform.protocol_version,
}
}
}
pub fn parse_request(line: &str) -> Result<Request, HostError> {
let invalid = |error: serde_json::Error| {
HostError::new("INVALID_REQUEST", format!("invalid request: {error}"))
};
let fields: HashMap<String, &RawValue> = serde_json::from_str(line).map_err(|error| {
let what = if error.is_data() {
"a request is a JSON object"
} else {
"invalid JSON"
};
HostError::new("INVALID_REQUEST", format!("{what}: {error}"))
})?;
let setting = |name: &str| -> Result<Option<Value>, HostError> {
let Some(raw) = fields.get(name) else {
return Ok(None);
};
if raw.get().len() > MAX_REQUEST_SETTING_BYTES {
return Err(HostError::invalid_input(format!(
"{name} is {} bytes; a request setting is at most {MAX_REQUEST_SETTING_BYTES}",
raw.get().len()
)));
}
serde_json::from_str(raw.get())
.map(Some)
.map_err(|error| HostError::new("INVALID_REQUEST", format!("invalid JSON: {error}")))
};
let version = fields
.get("protocolVersion")
.and_then(|raw| serde_json::from_str::<u64>(raw.get()).ok())
.ok_or_else(|| {
HostError::new(
"UNSUPPORTED_PROTOCOL",
"protocolVersion must be the integer 1",
)
})?;
if version != u64::from(PROTOCOL_VERSION) {
return Err(HostError::new(
"UNSUPPORTED_PROTOCOL",
format!("unsupported protocol major {version}; this host supports {PROTOCOL_VERSION}"),
));
}
let operation = fields
.get("operation")
.and_then(|raw| serde_json::from_str::<String>(raw.get()).ok());
let request = match operation.as_deref() {
Some("capabilities") => {
let CapabilitiesRequest {
protocol_version,
request_id,
} = serde_json::from_str(line).map_err(invalid)?;
Request::Capabilities {
protocol_version,
request_id,
}
}
Some("export") => {
if let Some(quality) = setting("videoQuality")? {
VideoQualityRequest::from_json(&quality).map_err(HostError::invalid_input)?;
}
if let Some(quality) = setting("audioQuality")? {
AudioQualityRequest::from_json(&quality).map_err(HostError::invalid_input)?;
}
if let Some(cache) = setting("renderCache")?.filter(|value| !value.is_null()) {
crate::render_cache::RenderCacheRequest::from_json(&cache)
.map_err(HostError::invalid_input)?;
}
if let Some(loudness) = setting("loudness")?.filter(|value| !value.is_null()) {
LoudnessRequest::from_json(&loudness).map_err(HostError::invalid_input)?;
}
if let Some(memory) = setting("memory")? {
serde_json::from_value::<crate::memory::MemoryRequest>(memory)
.map_err(|error| HostError::invalid_input(format!("memory: {error}")))?;
}
let export: ExportRequest = serde_json::from_str(line).map_err(invalid)?;
export.check()?;
Request::Export(Box::new(export))
}
Some("conform") => Request::Conform(serde_json::from_str(line).map_err(invalid)?),
Some(other) => {
return Err(HostError::new(
"INVALID_REQUEST",
format!(
"invalid request: unknown operation `{other}`, expected one of `capabilities`, `export`, `conform`"
),
));
}
None => {
return Err(HostError::new(
"INVALID_REQUEST",
"invalid request: missing field `operation`",
));
}
};
debug_assert_eq!(request.protocol_version(), PROTOCOL_VERSION);
if request.request_id().is_empty() {
return Err(HostError::new(
"INVALID_REQUEST",
"requestId must not be empty",
));
}
Ok(request)
}
const fn include_audio_by_default() -> bool {
true
}
fn render_cache<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<crate::render_cache::RenderCacheRequest>, D::Error> {
let value = Option::<Value>::deserialize(deserializer)?;
value
.filter(|value| !value.is_null())
.map(|value| {
crate::render_cache::RenderCacheRequest::from_json(&value)
.map_err(serde::de::Error::custom)
})
.transpose()
}
pub fn request_id_from_line(line: &str) -> String {
#[derive(Deserialize)]
struct Id {
#[serde(rename = "requestId")]
request_id: Option<String>,
}
serde_json::from_str::<Id>(line)
.ok()
.and_then(|id| id.request_id)
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "unknown".to_owned())
}
pub fn operation_from_line(line: &str) -> Option<String> {
#[derive(Deserialize)]
struct Operation {
operation: Option<String>,
}
serde_json::from_str::<Operation>(line)
.ok()
.and_then(|operation| operation.operation)
}
crate::tag::tags! {
pub fn gpu_timeout_tag = "GPU_TIMEOUT] ";
pub fn internal_tag = "INTERNAL] ";
pub fn decode_stalled_tag = "DECODE_STALLED] ";
pub fn encode_stalled_tag = "ENCODE_STALLED] ";
pub fn gpu_output_unavailable_tag = "GPU_OUTPUT_UNAVAILABLE] ";
pub fn copy_ineligible_tag = "COPY_INELIGIBLE] ";
fn code_tag_open = "code=";
}
pub const HARDWARE_DECODE_STALL_MARKER: &str = "holding hardware decode sessions";
pub const HARDWARE_ENCODE_STALL_MARKER: &str = "holding a hardware encode session";
pub const PROTOCOL_CODES: [&str; 22] = [
"UNSUPPORTED_PROTOCOL",
"INVALID_REQUEST",
"INVALID_INPUT",
"CONFORM_FAILED",
"MISSING_INPUT",
"INCOMPLETE_COMPOSITION",
"EFFECT_ERROR",
"GPU_INIT_FAILED",
"HARDWARE_DECODE_UNAVAILABLE",
"GPU_DEVICE_LOST",
"GPU_TIMEOUT",
"DECODE_STALLED",
"ENCODE_STALLED",
"COPY_INELIGIBLE",
"GPU_OUTPUT_UNAVAILABLE",
"ENCODE_ERROR",
"IO_ERROR",
"OUTPUT_IN_USE",
"CANCELLED",
"RESOURCE_LIMIT",
"INTERNAL",
"ALPHA2_ERROR",
];
pub fn code_tag(code: &'static str, message: impl Into<String>) -> String {
let message = message.into();
debug_assert!(
PROTOCOL_CODES.contains(&code),
"{code} is not a protocol code"
);
if message.contains(code_tag_open()) {
return message;
}
format!("{}{code}] {message}", code_tag_open())
}
pub fn missing_input(message: impl Into<String>) -> String {
code_tag("MISSING_INPUT", message)
}
pub fn cancelled(message: impl Into<String>) -> String {
code_tag("CANCELLED", message)
}
pub fn is_cancelled(message: &str) -> bool {
message.contains(&format!("{}CANCELLED] ", code_tag_open()))
}
pub fn names_device_fault(message: &str) -> bool {
message.contains(gpu_timeout_tag())
|| message.contains(&format!("{}GPU_DEVICE_LOST] ", code_tag_open()))
}
fn take_code_tag(mut message: String) -> (Option<&'static str>, String) {
let mut code = None;
while let Some(start) = message.find(code_tag_open()) {
let Some(end) = crate::tag::end(&message, start) else {
break;
};
let name = &message[start + code_tag_open().len()..end - 2];
if code.is_none() {
code = PROTOCOL_CODES.iter().copied().find(|known| *known == name);
}
message.replace_range(start..end, "");
}
(code, message)
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HostError {
pub code: &'static str,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub failure: Option<Value>,
#[serde(skip)]
pub evidence: Option<&'static str>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ProgressPhase {
Frames,
Finalizing,
}
impl ProgressPhase {
pub const fn as_str(self) -> &'static str {
match self {
Self::Frames => "frames",
Self::Finalizing => "finalizing",
}
}
}
pub fn finalizing(
progress: &mut dyn FnMut(Progress) -> Result<(), HostError>,
) -> impl FnMut(Progress) -> Result<(), HostError> + '_ {
move |event| {
progress(Progress {
phase: ProgressPhase::Finalizing,
..event
})
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Progress {
pub phase: ProgressPhase,
pub completed: u64,
pub total: u64,
}
impl Progress {
pub const fn frames(completed: u64, total: u64) -> Self {
Self {
phase: ProgressPhase::Frames,
completed,
total,
}
}
pub const fn finalizing(completed: u64, total: u64) -> Self {
Self {
phase: ProgressPhase::Finalizing,
completed,
total,
}
}
}
impl HostError {
pub fn new(code: &'static str, message: impl Into<String>) -> Self {
Self {
code,
message: crate::tag::untagged(&message.into()),
details: None,
failure: None,
evidence: None,
}
}
#[must_use]
pub fn with_failure(mut self, failure: Value) -> Self {
self.failure = Some(failure);
self
}
#[must_use]
pub fn with_details(mut self, details: Value) -> Self {
self.details = Some(details);
self
}
pub fn invalid_input(message: impl Into<String>) -> Self {
Self::new("INVALID_INPUT", message)
}
#[allow(clippy::needless_pass_by_value)] pub fn io(error: std::io::Error) -> Self {
Self::new("IO_ERROR", error.to_string())
}
#[allow(clippy::needless_pass_by_value)] pub fn json(error: serde_json::Error) -> Self {
Self::new("ALPHA2_ERROR", error.to_string())
}
pub fn classify(message: String) -> Self {
let (evidence, message) = crate::failure::take_evidence_tag(message);
let mut error = Self::classify_code(message);
error.evidence = evidence;
error
}
fn classify_code(message: String) -> Self {
let (code, message) = take_code_tag(crate::failure::strip_phase_tags(message));
for (tag, code) in [
(internal_tag(), "INTERNAL"),
(crate::memory::resource_limit_tag(), "RESOURCE_LIMIT"),
(copy_ineligible_tag(), "COPY_INELIGIBLE"),
(gpu_output_unavailable_tag(), "GPU_OUTPUT_UNAVAILABLE"),
(decode_stalled_tag(), "DECODE_STALLED"),
(encode_stalled_tag(), "ENCODE_STALLED"),
(gpu_timeout_tag(), "GPU_TIMEOUT"),
] {
if message.contains(tag) {
return Self::new(code, message);
}
}
Self::new(code.unwrap_or("ALPHA2_ERROR"), message)
}
}
#[derive(Copy, Clone, Deserialize)]
pub struct RateRequest {
pub num: u32,
pub den: u32,
}
#[derive(Copy, Clone, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum VideoCodecRequest {
#[default]
H264,
Hevc,
Vp9,
Av1,
Mpeg4Part2,
}
impl VideoCodecRequest {
pub const fn into_native(self) -> NativeVideoCodec {
match self {
Self::H264 => NativeVideoCodec::H264,
Self::Hevc => NativeVideoCodec::Hevc,
Self::Vp9 => NativeVideoCodec::Vp9,
Self::Av1 => NativeVideoCodec::Av1,
Self::Mpeg4Part2 => NativeVideoCodec::Mpeg4Part2,
}
}
}
pub const MAX_QUANTIZER: u32 = 255;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum VideoQualityRequest {
Level(QualityLevel),
Explicit(VideoRateControl),
}
impl Default for VideoQualityRequest {
fn default() -> Self {
Self::Level(QualityLevel::default())
}
}
impl VideoQualityRequest {
pub fn from_json(value: &Value) -> Result<Self, String> {
const FIELD: &str = "videoQuality";
match value {
Value::String(level) => level
.parse()
.map(Self::Level)
.map_err(|error| format!("{FIELD}: {error}")),
Value::Object(fields) => {
if let Some(key) = fields
.keys()
.find(|key| !matches!(key.as_str(), "bitrate" | "mode" | "quantizer"))
{
return Err(format!(
"{FIELD} has an unknown key \"{key}\"; an explicit rate takes bitrate, mode and quantizer"
));
}
let bitrate = fields
.get("bitrate")
.ok_or_else(|| format!("{FIELD}.bitrate is required in an explicit rate"))?;
let bitrate = bitrate.as_u64().filter(|bitrate| *bitrate > 0).ok_or_else(|| {
format!("{FIELD}.bitrate must be a positive integer number of bits per second, received {bitrate}")
})?;
let mode = match fields.get("mode") {
None => BitrateMode::default(),
Some(Value::String(mode)) => mode
.parse()
.map_err(|error| format!("{FIELD}.mode: {error}"))?,
Some(other) => {
return Err(format!(
"{FIELD}.mode must be \"constant\" or \"variable\", received {other}"
));
}
};
let quantizer = match fields.get("quantizer") {
None => None,
Some(quantizer) => Some(
quantizer
.as_u64()
.and_then(|quantizer| u32::try_from(quantizer).ok())
.filter(|quantizer| *quantizer <= MAX_QUANTIZER)
.ok_or_else(|| {
format!("{FIELD}.quantizer must be an integer from 0 to {MAX_QUANTIZER}, received {quantizer}")
})?,
),
};
VideoRateControl::bitrate(bitrate, mode)
.map(|rate| Self::Explicit(VideoRateControl { quantizer, ..rate }))
.map_err(|error| format!("{FIELD}.bitrate: {error}"))
}
other => Err(format!(
"{FIELD} must be a quality level name or an explicit rate object, received {other}"
)),
}
}
pub fn from_cli(value: &str) -> Result<Self, String> {
if value.bytes().all(|byte| byte.is_ascii_digit()) && !value.is_empty() {
let bitrate = value
.parse::<u64>()
.map_err(|_| format!("--video-quality bitrate {value} is out of range"))?;
return VideoRateControl::bitrate(bitrate, BitrateMode::default())
.map(Self::Explicit)
.map_err(|error| format!("--video-quality: {error}"));
}
value
.parse()
.map(Self::Level)
.map_err(|error| format!("--video-quality: {error}"))
}
}
impl<'de> Deserialize<'de> for VideoQualityRequest {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = Value::deserialize(deserializer)?;
Self::from_json(&value).map_err(serde::de::Error::custom)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct LoudnessRequest {
pub target_lufs: f64,
pub true_peak_ceiling_dbtp: f64,
}
impl LoudnessRequest {
pub fn new(target_lufs: f64, true_peak_ceiling_dbtp: f64) -> Result<Self, String> {
if !(target_lufs.is_finite() && (-60.0..=0.0).contains(&target_lufs)) {
return Err(format!(
"loudness.targetLufs must be within -60..=0, received {target_lufs}"
));
}
if !(true_peak_ceiling_dbtp.is_finite() && (-20.0..=0.0).contains(&true_peak_ceiling_dbtp))
{
return Err(format!(
"loudness.truePeakCeilingDbtp must be within -20..=0, received {true_peak_ceiling_dbtp}"
));
}
Ok(Self {
target_lufs,
true_peak_ceiling_dbtp,
})
}
pub fn from_json(value: &Value) -> Result<Self, String> {
let Value::Object(fields) = value else {
return Err(format!(
"loudness must be an object {{ targetLufs, truePeakCeilingDbtp }}, received {value}"
));
};
if let Some(key) = fields
.keys()
.find(|key| !matches!(key.as_str(), "targetLufs" | "truePeakCeilingDbtp"))
{
return Err(format!(
"loudness has an unknown key \"{key}\"; it takes targetLufs and truePeakCeilingDbtp"
));
}
let number = |key: &str| {
fields
.get(key)
.ok_or_else(|| format!("loudness.{key} is required"))?
.as_f64()
.ok_or_else(|| format!("loudness.{key} must be a number"))
};
Self::new(number("targetLufs")?, number("truePeakCeilingDbtp")?)
}
}
impl<'de> Deserialize<'de> for LoudnessRequest {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = Value::deserialize(deserializer)?;
Self::from_json(&value).map_err(serde::de::Error::custom)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum AudioQualityRequest {
Level(QualityLevel),
Explicit(u32),
}
impl Default for AudioQualityRequest {
fn default() -> Self {
Self::Level(QualityLevel::default())
}
}
impl AudioQualityRequest {
pub fn from_json(value: &Value) -> Result<Self, String> {
const FIELD: &str = "audioQuality";
match value {
Value::String(level) => level
.parse()
.map(Self::Level)
.map_err(|error| format!("{FIELD}: {error}")),
Value::Object(fields) => {
if let Some(key) = fields.keys().find(|key| key.as_str() != "bitrate") {
return Err(format!(
"{FIELD} has an unknown key \"{key}\"; an explicit rate takes bitrate"
));
}
let bitrate = fields
.get("bitrate")
.ok_or_else(|| format!("{FIELD}.bitrate is required in an explicit rate"))?;
bitrate
.as_u64()
.filter(|bitrate| *bitrate > 0)
.and_then(|bitrate| u32::try_from(bitrate).ok())
.map(Self::Explicit)
.ok_or_else(|| {
format!("{FIELD}.bitrate must be a positive integer number of bits per second, received {bitrate}")
})
}
other => Err(format!(
"{FIELD} must be a quality level name or an explicit rate object, received {other}"
)),
}
}
pub fn from_cli(value: &str) -> Result<Self, String> {
if !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()) {
return value
.parse::<u32>()
.ok()
.filter(|bitrate| *bitrate > 0)
.map(Self::Explicit)
.ok_or_else(|| {
format!("--audio-quality bitrate {value} must be a positive integer")
});
}
value
.parse()
.map(Self::Level)
.map_err(|error| format!("--audio-quality: {error}"))
}
}
impl<'de> Deserialize<'de> for AudioQualityRequest {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = Value::deserialize(deserializer)?;
Self::from_json(&value).map_err(serde::de::Error::custom)
}
}
#[derive(Copy, Clone, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AudioCodecRequest {
#[default]
Aac,
}
impl AudioCodecRequest {
pub const fn into_native(self) -> NativeAudioCodec {
match self {
Self::Aac => NativeAudioCodec::Aac,
}
}
}
pub fn decode_capabilities_json(
mode: &str,
device_ready: bool,
ten_bit: bool,
device_codecs: Option<&[&'static str]>,
) -> Value {
let capabilities = hardware_decode_capabilities();
json!({
"mode": mode,
"backend": capabilities.backend,
"deviceReady": device_ready,
"tenBit": ten_bit,
"deviceCodecs": device_codecs,
"codecs": capabilities.codecs.into_iter().map(|capability| json!({
"id": capability.codec.as_str(),
"decoder": capability.decoder_name,
"hardwareConfig": capability.hardware_config,
"softwareDecoder": capability.software_decoder,
})).collect::<Vec<_>>(),
})
}
pub fn codec_capabilities_json() -> Value {
let capabilities = codec_capabilities();
json!({
"container": "mp4",
"video": capabilities.mp4_video.into_iter().map(|capability| json!({
"id": capability.codec.as_str(),
"encoder": capability.encoder_name,
"encoderClass": capability.class.as_str(),
"hardwareAccelerated": capability.hardware_accelerated,
"preferred": capability.preferred,
"minimumWidth": capability.minimum_width,
"minimumHeight": capability.minimum_height,
})).collect::<Vec<_>>(),
"audio": capabilities.mp4_audio.into_iter().map(|capability| json!({
"id": capability.codec.as_str(),
"encoder": capability.encoder_name,
})).collect::<Vec<_>>(),
})
}