mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-04-24 16:41:07 -04:00
* initial ffprobe commit * Working slim down version ffprobe * Auto format ffprobe and deps source * Remove show_pixel_formats logic - Fix do_bitexact incorrect check in main after last changes - Fix some clangd warning * Remove show_* and print_format options and their respective logic * Rework ffprobe into simple_ffprobe - Simplify ffprobe logic into a simple program that gather and print a media file metadata * Reduce the amount of ffmpeg log messages while generating thumbnails * Fix completly wrong comments * mend * Start modeling ffmpeg extracted metadata on schema - Start porting ffprobe code to rust - Rename some references to media_data to exif_data * Finish modeling media info data - Add MediaProgram, MediaStream, MediaCodec, MediaVideoProps, MediaAudioProps, MediaSubtitleProps to Schema - Fix simple_ffproble to use its custom print_codec, instead of ffmpeg's impl * Add relation between MediaInfo and FilePath - Remove shared properties from MediaInfo and related structs - Implement Iterator for FFmpegDict * Fix and update schema * Data models and start populating MediaInfo in rust * Finish populating media info, chapters and program * Improve FFmpegFormatContext data raw pointer access - Implement stream data gathering * Impl FFmpegCodecContext, retrieve codec information - Improve some unsafe pointer uses - Impl from FFmpegFormatContext to MediaInfo conversion * Fix FFmpegDict Drop * Fix some crago warnings * Impl retrieval of video props - Fix C char* to Rust String convertion * Impl retrieval of audio and subtitle props - Fill props for MediaCodec * Remove simple_ffprobe now that the Rust impl is done * Fix schema to match actually retrieved media info - Fix import some FFmpeg constants instead of directly using values * Rework movie_decoder - Re-implement create_scale_string and add support anamorphic video - Improve C pointer access for FFmpegFormatContext and FFmpegCodecContext - Use newer FFmpeg abstractions in movie_decoder * Fix incorrect props when initializing MovieDecoder * Remove unecessary lifetimes * Added more native wrappers for some FFmpeg native objects used in movie_decoder * Remove FFmpegPacket - Some more improvements to movie_decoder * WIP * Some small fixes * More fixes Rename movie_decoder to frame_decoder Remove more references to film_strips * fmt * Fix duplicate migration for job error changes * fix rebase * Solving segfaults, fuck C lang Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com> * Update rust to version 1.77 - Pin rust version with rust-toolchain.toml - Change from dtolnay/rust-toolchain to IronCoreLabs/rust-toolchain for rust-toolchain support - Remove unused function and imports - Replace most CString uses with new c literal string * More segfault solving and other minor fixes Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com> * Fix ffmpeg rotation filter breaking portrait video thumbnails #2150 - Plus some other misc fixes * Auto format * Retrieve video/audio metadata on frontend * Auto format * First draft on ffmpeg data save on db Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com> * Fix some incorrect changes to prisma schema * Some fixes for the FFmpegData schema - Expand logic to save FFmpegData to db * A ton of things Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com> * Integrating ffmpeg media data in jobs and API * Rspc can't BigInt * 🙄 * Add initial ffmpeg metadata entries to Inspector - Fix ephemeral metadata api to match the files metadata api call * Fix Inspector not showing ffmpeg metadata * Add bitrate, start time and chapters video metadata to Inspector - Fix backend BigInt conversion incorrectly using i32 instead of u32 - Change FFmpegFormatContext/FFmpegMetaData bit_rate to i64 - Rename byteSize to humanizeSize - Expand humanizeSize logic to allow handling bits and Binary units - Move capitalize to @sd/client utils * Solving some issues * Fix ffmpeg probe getting incorrect stream id and breaking database unique constraint - Fix humanizeSize breaking when receiving floating numbers - Fix incorrect equality in StatCard - Fix unhandled error in Dialog when trying to remove an unknown dialog * fmt * small improvements - Remove some unecessary recursion_limit directive - Remove unused app_image releated functions - Fix metadata query enabled flag * Add migration for ffmpeg media data * Fix cypress test * Requested changes * Implement feedback - Update locale keys for all languages - Add pnpm command to update all language keys * Fix thumb reactivity in non indexed locations --------- Co-authored-by: Ericson Soares <ericson.ds999@gmail.com> Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com>
461 lines
13 KiB
Rust
461 lines
13 KiB
Rust
use crate::{
|
|
error::{Error, FFmpegError},
|
|
model::{FFmpegAudioProps, FFmpegCodec, FFmpegProps, FFmpegSubtitleProps, FFmpegVideoProps},
|
|
utils::check_error,
|
|
};
|
|
|
|
use std::{
|
|
ffi::{CStr, CString},
|
|
ptr,
|
|
};
|
|
|
|
use ffmpeg_sys_next::{
|
|
av_bprint_finalize, av_bprint_init, av_channel_layout_describe_bprint, av_chroma_location_name,
|
|
av_color_primaries_name, av_color_range_name, av_color_space_name, av_color_transfer_name,
|
|
av_fourcc_make_string, av_get_bits_per_sample, av_get_bytes_per_sample,
|
|
av_get_media_type_string, av_get_pix_fmt_name, av_get_sample_fmt_name, av_pix_fmt_desc_get,
|
|
av_reduce, avcodec_alloc_context3, avcodec_flush_buffers, avcodec_free_context,
|
|
avcodec_get_name, avcodec_open2, avcodec_parameters_to_context, avcodec_profile_name,
|
|
avcodec_receive_frame, avcodec_send_packet, AVBPrint, AVChromaLocation, AVCodec,
|
|
AVCodecContext, AVCodecParameters, AVColorPrimaries, AVColorRange, AVColorSpace,
|
|
AVColorTransferCharacteristic, AVFieldOrder, AVFrame, AVMediaType, AVPacket, AVPixelFormat,
|
|
AVRational, AVSampleFormat, AVERROR, AVERROR_EOF, AV_FOURCC_MAX_STRING_SIZE,
|
|
FF_CODEC_PROPERTY_CLOSED_CAPTIONS, FF_CODEC_PROPERTY_FILM_GRAIN, FF_CODEC_PROPERTY_LOSSLESS,
|
|
};
|
|
use libc::EAGAIN;
|
|
|
|
pub struct FFmpegCodecContext(*mut AVCodecContext);
|
|
|
|
impl FFmpegCodecContext {
|
|
pub(crate) fn new() -> Result<Self, Error> {
|
|
let ptr = unsafe { avcodec_alloc_context3(ptr::null_mut()) };
|
|
if ptr.is_null() {
|
|
Err(FFmpegError::VideoCodecAllocation)?;
|
|
}
|
|
|
|
Ok(Self(ptr))
|
|
}
|
|
|
|
pub(crate) fn as_ref(&self) -> &AVCodecContext {
|
|
unsafe { self.0.as_ref() }.expect("initialized on struct creation")
|
|
}
|
|
|
|
pub(crate) fn as_mut(&mut self) -> &mut AVCodecContext {
|
|
unsafe { self.0.as_mut() }.expect("initialized on struct creation")
|
|
}
|
|
|
|
pub(crate) fn parameters_to_context(
|
|
&mut self,
|
|
codec_params: &AVCodecParameters,
|
|
) -> Result<&Self, Error> {
|
|
check_error(
|
|
unsafe { avcodec_parameters_to_context(self.as_mut(), codec_params) },
|
|
"Fail to fill the codec context with codec parameters",
|
|
)?;
|
|
|
|
Ok(self)
|
|
}
|
|
|
|
pub(crate) fn open2(&mut self, video_codec: &AVCodec) -> Result<&Self, Error> {
|
|
check_error(
|
|
unsafe { avcodec_open2(self.as_mut(), video_codec, ptr::null_mut()) },
|
|
"Failed to open video codec",
|
|
)?;
|
|
|
|
Ok(self)
|
|
}
|
|
|
|
pub(crate) fn flush(&mut self) {
|
|
unsafe { avcodec_flush_buffers(self.as_mut()) };
|
|
}
|
|
|
|
pub(crate) fn send_packet(&mut self, packet: *mut AVPacket) -> Result<bool, FFmpegError> {
|
|
match unsafe { avcodec_send_packet(self.as_mut(), packet) } {
|
|
AVERROR_EOF => Ok(false),
|
|
ret if ret == AVERROR(EAGAIN) => Err(FFmpegError::Again),
|
|
ret if ret < 0 => Err(FFmpegError::from(ret)),
|
|
_ => Ok(true),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn receive_frame(&mut self, frame: *mut AVFrame) -> Result<bool, FFmpegError> {
|
|
match unsafe { avcodec_receive_frame(self.as_mut(), frame) } {
|
|
AVERROR_EOF => Ok(false),
|
|
ret if ret == AVERROR(EAGAIN) => Err(FFmpegError::Again),
|
|
ret if ret < 0 => Err(FFmpegError::from(ret)),
|
|
_ => Ok(true),
|
|
}
|
|
}
|
|
|
|
fn kind(&self) -> (Option<String>, Option<String>) {
|
|
let kind = unsafe { av_get_media_type_string(self.as_ref().codec_type).as_ref() }
|
|
.map(|media_type| unsafe { CStr::from_ptr(media_type) });
|
|
|
|
let sub_kind = unsafe { self.as_ref().codec.as_ref() }
|
|
.and_then(|codec| unsafe { codec.name.as_ref() })
|
|
.map(|name| unsafe { CStr::from_ptr(name) })
|
|
.and_then(|sub_kind| {
|
|
if let Some(kind) = kind {
|
|
if kind == sub_kind {
|
|
return None;
|
|
}
|
|
}
|
|
|
|
Some(String::from_utf8_lossy(sub_kind.to_bytes()).to_string())
|
|
});
|
|
|
|
(
|
|
kind.map(|cstr| String::from_utf8_lossy(cstr.to_bytes()).to_string()),
|
|
sub_kind,
|
|
)
|
|
}
|
|
|
|
fn name(&self) -> Option<String> {
|
|
unsafe { avcodec_get_name(self.as_ref().codec_id).as_ref() }.map(|codec_name| {
|
|
let cstr = unsafe { CStr::from_ptr(codec_name) };
|
|
String::from_utf8_lossy(cstr.to_bytes()).to_string()
|
|
})
|
|
}
|
|
|
|
fn profile(&self) -> Option<String> {
|
|
if self.as_ref().profile == 0 {
|
|
None
|
|
} else {
|
|
unsafe { avcodec_profile_name(self.as_ref().codec_id, self.as_ref().profile).as_ref() }
|
|
.map(|profile| {
|
|
let cstr = unsafe { CStr::from_ptr(profile) };
|
|
String::from_utf8_lossy(cstr.to_bytes()).to_string()
|
|
})
|
|
}
|
|
}
|
|
|
|
fn tag(&self) -> Option<String> {
|
|
if self.as_ref().codec_tag != 0 {
|
|
CString::new(vec![
|
|
0;
|
|
usize::try_from(AV_FOURCC_MAX_STRING_SIZE).expect(
|
|
"AV_FOURCC_MAX_STRING_SIZE is 32, must fit in an usize"
|
|
)
|
|
])
|
|
.ok()
|
|
.map(|buffer| {
|
|
let tag = unsafe {
|
|
CString::from_raw(av_fourcc_make_string(
|
|
buffer.into_raw(),
|
|
self.as_ref().codec_tag,
|
|
))
|
|
};
|
|
String::from_utf8_lossy(tag.as_bytes()).to_string()
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn bit_rate(&self) -> i32 {
|
|
// TODO: use i64 instead of i32 when rspc supports it
|
|
let ctx = self.as_ref();
|
|
match self.as_ref().codec_type {
|
|
AVMediaType::AVMEDIA_TYPE_VIDEO
|
|
| AVMediaType::AVMEDIA_TYPE_DATA
|
|
| AVMediaType::AVMEDIA_TYPE_SUBTITLE
|
|
| AVMediaType::AVMEDIA_TYPE_ATTACHMENT => ctx.bit_rate.try_into().unwrap_or_default(),
|
|
AVMediaType::AVMEDIA_TYPE_AUDIO => {
|
|
let bits_per_sample = unsafe { av_get_bits_per_sample(ctx.codec_id) };
|
|
if bits_per_sample != 0 {
|
|
let bit_rate = ctx.sample_rate * ctx.ch_layout.nb_channels;
|
|
if bit_rate <= i32::MAX / bits_per_sample {
|
|
return bit_rate * (bits_per_sample);
|
|
}
|
|
}
|
|
ctx.bit_rate.try_into().unwrap_or_default()
|
|
}
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
fn video_props(&self) -> Option<FFmpegVideoProps> {
|
|
let ctx = self.as_ref();
|
|
if ctx.codec_type != AVMediaType::AVMEDIA_TYPE_VIDEO {
|
|
return None;
|
|
}
|
|
|
|
let pixel_format = extract_pixel_format(ctx);
|
|
|
|
let bits_per_channel = extract_bits_per_channel(ctx);
|
|
|
|
let color_range = extract_color_range(ctx);
|
|
|
|
let (color_space, color_primaries, color_transfer) = extract_colors(ctx);
|
|
|
|
// Field Order
|
|
let field_order = extract_field_order(ctx);
|
|
|
|
// Chroma Sample Location
|
|
let chroma_location = extract_chroma_location(ctx);
|
|
|
|
let width = ctx.width;
|
|
let height = ctx.height;
|
|
|
|
let (aspect_ratio_num, aspect_ratio_den) = extract_aspect_ratio(ctx, width, height);
|
|
|
|
let mut properties = vec![];
|
|
if ctx.properties & (FF_CODEC_PROPERTY_LOSSLESS.unsigned_abs()) != 0 {
|
|
properties.push("Closed Captions".to_string());
|
|
}
|
|
if ctx.properties & (FF_CODEC_PROPERTY_CLOSED_CAPTIONS.unsigned_abs()) != 0 {
|
|
properties.push("Film Grain".to_string());
|
|
}
|
|
if ctx.properties & (FF_CODEC_PROPERTY_FILM_GRAIN.unsigned_abs()) != 0 {
|
|
properties.push("lossless".to_string());
|
|
}
|
|
|
|
Some(FFmpegVideoProps {
|
|
pixel_format,
|
|
color_range,
|
|
bits_per_channel,
|
|
color_space,
|
|
color_primaries,
|
|
color_transfer,
|
|
field_order,
|
|
chroma_location,
|
|
width,
|
|
height,
|
|
aspect_ratio_num,
|
|
aspect_ratio_den,
|
|
properties,
|
|
})
|
|
}
|
|
|
|
fn audio_props(&self) -> Option<FFmpegAudioProps> {
|
|
let ctx = self.as_ref();
|
|
if ctx.codec_type != AVMediaType::AVMEDIA_TYPE_AUDIO {
|
|
return None;
|
|
}
|
|
|
|
let sample_rate = if ctx.sample_rate > 0 {
|
|
Some(ctx.sample_rate)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let mut bprint = AVBPrint {
|
|
str_: ptr::null_mut(),
|
|
len: 0,
|
|
size: 0,
|
|
size_max: 0,
|
|
reserved_internal_buffer: [0; 1],
|
|
reserved_padding: [0; 1000],
|
|
};
|
|
unsafe {
|
|
av_bprint_init(&mut bprint, 0, u32::MAX /* AV_BPRINT_SIZE_UNLIMITED */);
|
|
};
|
|
let mut channel_layout = ptr::null_mut();
|
|
let channel_layout =
|
|
if unsafe { av_channel_layout_describe_bprint(&ctx.ch_layout, &mut bprint) } < 0
|
|
|| unsafe { av_bprint_finalize(&mut bprint, &mut channel_layout) } < 0
|
|
|| channel_layout.is_null()
|
|
{
|
|
None
|
|
} else {
|
|
let cstr = unsafe { CStr::from_ptr(channel_layout) };
|
|
Some(String::from_utf8_lossy(cstr.to_bytes()).to_string())
|
|
};
|
|
|
|
let sample_format = if ctx.sample_fmt == AVSampleFormat::AV_SAMPLE_FMT_NONE {
|
|
None
|
|
} else {
|
|
unsafe { av_get_sample_fmt_name(ctx.sample_fmt).as_ref() }.map(|sample_fmt| {
|
|
let cstr = unsafe { CStr::from_ptr(sample_fmt) };
|
|
String::from_utf8_lossy(cstr.to_bytes()).to_string()
|
|
})
|
|
};
|
|
|
|
let bit_per_sample = if ctx.bits_per_raw_sample > 0
|
|
&& ctx.bits_per_raw_sample != unsafe { av_get_bytes_per_sample(ctx.sample_fmt) } * 8
|
|
{
|
|
Some(ctx.bits_per_raw_sample)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Some(FFmpegAudioProps {
|
|
delay: ctx.initial_padding,
|
|
padding: ctx.trailing_padding,
|
|
sample_rate,
|
|
sample_format,
|
|
bit_per_sample,
|
|
channel_layout,
|
|
})
|
|
}
|
|
|
|
fn subtitle_props(&self) -> Option<FFmpegSubtitleProps> {
|
|
if self.as_ref().codec_type != AVMediaType::AVMEDIA_TYPE_SUBTITLE {
|
|
return None;
|
|
}
|
|
|
|
Some(FFmpegSubtitleProps {
|
|
width: self.as_ref().width,
|
|
height: self.as_ref().height,
|
|
})
|
|
}
|
|
|
|
fn props(&self) -> Option<FFmpegProps> {
|
|
match self.as_ref().codec_type {
|
|
AVMediaType::AVMEDIA_TYPE_VIDEO => self.video_props().map(FFmpegProps::Video),
|
|
AVMediaType::AVMEDIA_TYPE_AUDIO => self.audio_props().map(FFmpegProps::Audio),
|
|
AVMediaType::AVMEDIA_TYPE_SUBTITLE => self.subtitle_props().map(FFmpegProps::Subtitle),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn extract_aspect_ratio(
|
|
ctx: &AVCodecContext,
|
|
width: i32,
|
|
height: i32,
|
|
) -> (Option<i32>, Option<i32>) {
|
|
if ctx.sample_aspect_ratio.num == 0 {
|
|
(None, None)
|
|
} else {
|
|
let mut display_aspect_ratio = AVRational { num: 0, den: 0 };
|
|
let num = i64::from(width * ctx.sample_aspect_ratio.num);
|
|
let den = i64::from(height * ctx.sample_aspect_ratio.den);
|
|
let max = 1024 * 1024;
|
|
unsafe {
|
|
av_reduce(
|
|
&mut display_aspect_ratio.num,
|
|
&mut display_aspect_ratio.den,
|
|
num,
|
|
den,
|
|
max,
|
|
);
|
|
}
|
|
|
|
(
|
|
Some(display_aspect_ratio.num),
|
|
Some(display_aspect_ratio.den),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn extract_chroma_location(ctx: &AVCodecContext) -> Option<String> {
|
|
if ctx.chroma_sample_location == AVChromaLocation::AVCHROMA_LOC_UNSPECIFIED {
|
|
None
|
|
} else {
|
|
unsafe { av_chroma_location_name(ctx.chroma_sample_location).as_ref() }.map(
|
|
|chroma_location| {
|
|
let cstr = unsafe { CStr::from_ptr(chroma_location) };
|
|
String::from_utf8_lossy(cstr.to_bytes()).to_string()
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
fn extract_field_order(ctx: &AVCodecContext) -> Option<String> {
|
|
if ctx.field_order == AVFieldOrder::AV_FIELD_UNKNOWN {
|
|
None
|
|
} else {
|
|
Some(
|
|
(match ctx.field_order {
|
|
AVFieldOrder::AV_FIELD_TT => "top first",
|
|
AVFieldOrder::AV_FIELD_BB => "bottom first",
|
|
AVFieldOrder::AV_FIELD_TB => "top coded first (swapped)",
|
|
AVFieldOrder::AV_FIELD_BT => "bottom coded first (swapped)",
|
|
_ => "progressive",
|
|
})
|
|
.to_string(),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn extract_colors(ctx: &AVCodecContext) -> (Option<String>, Option<String>, Option<String>) {
|
|
if ctx.colorspace == AVColorSpace::AVCOL_SPC_UNSPECIFIED
|
|
&& ctx.color_primaries == AVColorPrimaries::AVCOL_PRI_UNSPECIFIED
|
|
&& ctx.color_trc == AVColorTransferCharacteristic::AVCOL_TRC_UNSPECIFIED
|
|
{
|
|
(None, None, None)
|
|
} else {
|
|
let color_space =
|
|
unsafe { av_color_space_name(ctx.colorspace).as_ref() }.map(|color_space| {
|
|
let cstr = unsafe { CStr::from_ptr(color_space) };
|
|
String::from_utf8_lossy(cstr.to_bytes()).to_string()
|
|
});
|
|
let color_primaries = unsafe { av_color_primaries_name(ctx.color_primaries).as_ref() }.map(
|
|
|color_primaries| {
|
|
let cstr = unsafe { CStr::from_ptr(color_primaries) };
|
|
String::from_utf8_lossy(cstr.to_bytes()).to_string()
|
|
},
|
|
);
|
|
let color_transfer =
|
|
unsafe { av_color_transfer_name(ctx.color_trc).as_ref() }.map(|color_transfer| {
|
|
let cstr = unsafe { CStr::from_ptr(color_transfer) };
|
|
String::from_utf8_lossy(cstr.to_bytes()).to_string()
|
|
});
|
|
|
|
(color_space, color_primaries, color_transfer)
|
|
}
|
|
}
|
|
|
|
fn extract_color_range(ctx: &AVCodecContext) -> Option<String> {
|
|
if ctx.color_range == AVColorRange::AVCOL_RANGE_UNSPECIFIED {
|
|
None
|
|
} else {
|
|
unsafe { av_color_range_name(ctx.color_range).as_ref() }.map(|color_range| {
|
|
let cstr = unsafe { CStr::from_ptr(color_range) };
|
|
String::from_utf8_lossy(cstr.to_bytes()).to_string()
|
|
})
|
|
}
|
|
}
|
|
|
|
fn extract_bits_per_channel(ctx: &AVCodecContext) -> Option<i32> {
|
|
if ctx.bits_per_raw_sample == 0 || ctx.pix_fmt == AVPixelFormat::AV_PIX_FMT_NONE {
|
|
None
|
|
} else {
|
|
unsafe { av_pix_fmt_desc_get(ctx.pix_fmt).as_ref() }.and_then(|pix_fmt_desc| {
|
|
let comp = pix_fmt_desc.comp[0];
|
|
if ctx.bits_per_raw_sample < comp.depth {
|
|
Some(ctx.bits_per_raw_sample)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
fn extract_pixel_format(ctx: &AVCodecContext) -> Option<String> {
|
|
if ctx.pix_fmt == AVPixelFormat::AV_PIX_FMT_NONE {
|
|
None
|
|
} else {
|
|
unsafe { av_get_pix_fmt_name(ctx.pix_fmt).as_ref() }.map(|pixel_format| {
|
|
let cstr = unsafe { CStr::from_ptr(pixel_format) };
|
|
String::from_utf8_lossy(cstr.to_bytes()).to_string()
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Drop for FFmpegCodecContext {
|
|
fn drop(&mut self) {
|
|
if !self.0.is_null() {
|
|
unsafe { avcodec_free_context(&mut self.0) };
|
|
self.0 = ptr::null_mut();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<&FFmpegCodecContext> for FFmpegCodec {
|
|
fn from(ctx: &FFmpegCodecContext) -> Self {
|
|
let (kind, sub_kind) = ctx.kind();
|
|
|
|
Self {
|
|
kind,
|
|
sub_kind,
|
|
name: ctx.name(),
|
|
profile: ctx.profile(),
|
|
tag: ctx.tag(),
|
|
bit_rate: ctx.bit_rate(),
|
|
props: ctx.props(),
|
|
}
|
|
}
|
|
}
|