use std::{
io::{Read, Seek},
sync::{Arc, atomic::AtomicU64},
};
use bon::Builder;
use kithara_bufpool::{BytePool, PcmPool};
use kithara_stream::{AudioCodec, ContainerFormat, MediaInfo, SegmentLayout, SharedHooks};
use super::probe::{
ProbeHint, codec_from_mp4_fourcc, container_from_extension, probe_codec,
resolve_codec_container,
};
use crate::{
Decoder,
error::{DecodeError, DecodeResult},
mp4::sniff_mp4_codec,
traits::BoxedSource,
};
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DecoderBackend {
#[cfg(all(feature = "apple", any(target_os = "macos", target_os = "ios")))]
#[cfg_attr(
all(
not(feature = "symphonia"),
feature = "apple",
any(target_os = "macos", target_os = "ios")
),
default
)]
Apple,
#[cfg(all(feature = "android", target_os = "android"))]
#[cfg_attr(
all(
not(feature = "symphonia"),
feature = "android",
target_os = "android",
not(all(feature = "apple", any(target_os = "macos", target_os = "ios")))
),
default
)]
Android,
#[cfg(feature = "symphonia")]
#[default]
Symphonia,
}
impl std::fmt::Display for DecoderBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(feature = "apple", any(target_os = "macos", target_os = "ios")))]
Self::Apple => f.write_str("apple"),
#[cfg(all(feature = "android", target_os = "android"))]
Self::Android => f.write_str("android"),
#[cfg(feature = "symphonia")]
Self::Symphonia => f.write_str("symphonia"),
}
}
}
#[derive(Clone, Builder)]
#[builder(state_mod(vis = "pub"))]
#[non_exhaustive]
pub struct DecoderConfig {
#[builder(default)]
pub backend: DecoderBackend,
pub byte_len_handle: Option<Arc<AtomicU64>>,
pub byte_pool: Option<BytePool>,
pub hint: Option<String>,
pub hooks: Option<SharedHooks>,
pub pcm_pool: Option<PcmPool>,
pub segment_layout: Option<Arc<dyn SegmentLayout>>,
#[builder(default = true)]
pub gapless: bool,
#[builder(default)]
pub epoch: u64,
}
impl Default for DecoderConfig {
fn default() -> Self {
Self::builder().build()
}
}
pub struct DecoderFactory;
impl DecoderFactory {
pub(crate) fn create<R>(
source: R,
hint: &ProbeHint,
config: &DecoderConfig,
) -> DecodeResult<Box<dyn Decoder>>
where
R: Read + Seek + Send + Sync + 'static,
{
let source: BoxedSource = Box::new(source);
Self::dispatch_backend(source, hint, config)
}
pub fn create_from_media_info<R>(
source: R,
media_info: &MediaInfo,
config: &DecoderConfig,
) -> DecodeResult<Box<dyn Decoder>>
where
R: Read + Seek + Send + Sync + 'static,
{
tracing::debug!(?media_info, "create_from_media_info called");
let hint = ProbeHint {
codec: media_info.codec,
container: media_info.container,
extension: None,
mime: None,
};
Self::create(source, &hint, config)
}
pub fn create_with_probe<R>(
source: R,
hint: Option<&str>,
config: &DecoderConfig,
) -> DecodeResult<Box<dyn Decoder>>
where
R: Read + Seek + Send + Sync + 'static,
{
let mut source = source;
let mut probe_hint = ProbeHint {
container: hint.and_then(container_from_extension),
extension: hint.map(String::from),
..Default::default()
};
if matches!(
probe_hint.container,
Some(ContainerFormat::Mp4 | ContainerFormat::Fmp4)
) && let Some(codec) = sniff_mp4_codec(&mut source).and_then(codec_from_mp4_fourcc)
{
probe_hint.codec = Some(codec);
}
probe_codec(&probe_hint)?;
Self::create(source, &probe_hint, config)
}
pub(super) fn dispatch_backend(
source: BoxedSource,
hint: &ProbeHint,
config: &DecoderConfig,
) -> DecodeResult<Box<dyn Decoder>> {
let (codec, container) = resolve_codec_container(hint)?;
tracing::debug!(
?codec,
?container,
backend = ?config.backend,
"DecoderFactory::create called"
);
match config.backend {
#[cfg(all(feature = "apple", any(target_os = "macos", target_os = "ios")))]
DecoderBackend::Apple => create_apple(source, codec, container, config),
#[cfg(all(feature = "android", target_os = "android"))]
DecoderBackend::Android => create_android(source, codec, container, config),
#[cfg(feature = "symphonia")]
DecoderBackend::Symphonia => create_symphonia(source, codec, container, config),
}
}
}
#[cfg(all(feature = "apple", any(target_os = "macos", target_os = "ios")))]
fn create_apple(
source: BoxedSource,
codec: AudioCodec,
container: Option<ContainerFormat>,
config: &DecoderConfig,
) -> DecodeResult<Box<dyn Decoder>> {
use crate::apple::AppleCodec;
if should_use_segment_aware(codec, container, config)
&& let Some(layout) = config.segment_layout.clone()
{
if AppleCodec::supports(codec) {
tracing::debug!(
?codec,
"fmp4_segment: dispatching to segment-aware Apple HW codec path"
);
let gapless = config.gapless;
return build_fmp4_segment_decoder(source, layout, config, |track| {
AppleCodec::open_with_config(track, gapless)
});
}
#[cfg(feature = "symphonia")]
return create_fmp4_segment_symphonia(source, codec, layout, config);
#[cfg(not(feature = "symphonia"))]
{
let _ = layout;
return Err(DecodeError::UnsupportedCodec(codec));
}
}
if apple_standalone_supports(codec, container) {
tracing::debug!(
?codec,
?container,
"apple-standalone: routing via AudioFileServices"
);
return build_apple_standalone_decoder(source, codec, container, config);
}
#[cfg(feature = "symphonia")]
return create_symphonia(source, codec, container, config);
#[cfg(not(feature = "symphonia"))]
{
let _ = (source, container, config);
Err(DecodeError::UnsupportedCodec(codec))
}
}
#[cfg(all(feature = "apple", any(target_os = "macos", target_os = "ios")))]
fn apple_standalone_supports(codec: AudioCodec, container: Option<ContainerFormat>) -> bool {
crate::apple::AppleAudioFileDemuxer::supports(codec, container)
}
#[cfg(all(feature = "apple", any(target_os = "macos", target_os = "ios")))]
fn build_apple_standalone_decoder(
mut source: BoxedSource,
codec: AudioCodec,
container: Option<ContainerFormat>,
config: &DecoderConfig,
) -> DecodeResult<Box<dyn Decoder>> {
use crate::{
apple::{AppleAudioFileDemuxer, AppleCodec},
composed::ComposedDecoder,
demuxer::Demuxer,
gapless::scoped_probe,
};
let probed_gapless = if config.gapless {
scoped_probe(&mut *source, codec)?
} else {
None
};
let mut demuxer = AppleAudioFileDemuxer::open_for(source, codec, container)?;
if probed_gapless.is_some() {
demuxer.set_gapless(probed_gapless);
}
let codec_impl = AppleCodec::open_with_config(demuxer.track_info(), config.gapless)?;
let pool = config
.pcm_pool
.clone()
.unwrap_or_else(|| PcmPool::default().clone());
let decoder = ComposedDecoder::new(
demuxer,
codec_impl,
pool,
config.epoch,
config.byte_len_handle.clone(),
config.hooks.clone(),
);
Ok(Box::new(decoder))
}
#[cfg(all(feature = "android", target_os = "android"))]
fn create_android(
source: BoxedSource,
codec: AudioCodec,
container: Option<ContainerFormat>,
config: &DecoderConfig,
) -> DecodeResult<Box<dyn Decoder>> {
use crate::android::AndroidCodec;
if should_use_segment_aware(codec, container, config)
&& let Some(layout) = config.segment_layout.clone()
{
if AndroidCodec::supports(codec) {
tracing::debug!(
?codec,
"fmp4_segment: dispatching to segment-aware Android HW codec path"
);
return build_fmp4_segment_decoder(source, layout, config, |track| {
AndroidCodec::open_with_config(track)
});
}
#[cfg(feature = "symphonia")]
return create_fmp4_segment_symphonia(source, codec, layout, config);
#[cfg(not(feature = "symphonia"))]
{
let _ = layout;
return Err(DecodeError::UnsupportedCodec(codec));
}
}
if android_standalone_supports(codec, container) {
tracing::debug!(
?codec,
?container,
"android-standalone: routing via AMediaExtractor"
);
return build_android_standalone_decoder(source, codec, container, config);
}
#[cfg(feature = "symphonia")]
return create_symphonia(source, codec, container, config);
#[cfg(not(feature = "symphonia"))]
{
let _ = (source, container, config);
Err(DecodeError::UnsupportedCodec(codec))
}
}
#[cfg(all(feature = "android", target_os = "android"))]
fn android_standalone_supports(codec: AudioCodec, container: Option<ContainerFormat>) -> bool {
matches!(
(codec, container),
(AudioCodec::Pcm, Some(ContainerFormat::Wav))
| (AudioCodec::Mp3, Some(ContainerFormat::MpegAudio))
| (AudioCodec::Alac, Some(ContainerFormat::Mp4))
)
}
#[cfg(all(feature = "android", target_os = "android"))]
fn build_android_standalone_decoder(
source: BoxedSource,
codec: AudioCodec,
container: Option<ContainerFormat>,
config: &DecoderConfig,
) -> DecodeResult<Box<dyn Decoder>> {
use crate::{
android::{AndroidCodec, AndroidMediaExtractorDemuxer},
composed::ComposedDecoder,
demuxer::Demuxer,
};
let demuxer = match (codec, container) {
(AudioCodec::Pcm, Some(ContainerFormat::Wav)) => {
AndroidMediaExtractorDemuxer::open_wav(source)?
}
(AudioCodec::Mp3, Some(ContainerFormat::MpegAudio)) => {
AndroidMediaExtractorDemuxer::open_mp3(source)?
}
(AudioCodec::Alac, Some(ContainerFormat::Mp4)) => {
AndroidMediaExtractorDemuxer::open_alac_m4a(source)?
}
_ => return Err(DecodeError::UnsupportedCodec(codec)),
};
let codec_impl = AndroidCodec::open_with_config(demuxer.track_info())?;
let pool = config
.pcm_pool
.clone()
.unwrap_or_else(|| PcmPool::default().clone());
let decoder = ComposedDecoder::new(
demuxer,
codec_impl,
pool,
config.epoch,
config.byte_len_handle.clone(),
config.hooks.clone(),
);
Ok(Box::new(decoder))
}
#[cfg(feature = "symphonia")]
fn create_symphonia(
source: BoxedSource,
codec: AudioCodec,
container: Option<ContainerFormat>,
config: &DecoderConfig,
) -> DecodeResult<Box<dyn Decoder>> {
if should_use_segment_aware(codec, container, config)
&& let Some(layout) = config.segment_layout.clone()
{
return create_fmp4_segment_symphonia(source, codec, layout, config);
}
create_file_symphonia_universal(source, codec, container, config)
}
#[cfg(feature = "symphonia")]
fn create_file_symphonia_universal(
mut source: BoxedSource,
codec: AudioCodec,
container: Option<ContainerFormat>,
config: &DecoderConfig,
) -> DecodeResult<Box<dyn Decoder>> {
use crate::{
composed::ComposedDecoder,
demuxer::Demuxer,
gapless::scoped_probe,
symphonia::{SymphoniaCodec, SymphoniaConfig, SymphoniaDemuxer},
};
tracing::debug!(
?codec,
?container,
"file-symphonia: dispatching to ComposedDecoder<SymphoniaDemuxer, SymphoniaCodec>"
);
let probed_gapless = if config.gapless {
scoped_probe(&mut *source, codec)?
} else {
None
};
let (mut demuxer, _byte_len) = SymphoniaDemuxer::open_file(
source,
config.hint.clone(),
container,
config.byte_len_handle.clone(),
config.segment_layout.clone(),
)?;
if probed_gapless.is_some() {
demuxer.set_gapless(probed_gapless);
}
let symphonia_config = SymphoniaConfig {
gapless: config.gapless,
..Default::default()
};
let codec_impl = if SymphoniaCodec::supports(codec) {
SymphoniaCodec::open_with_config(demuxer.track_info(), &symphonia_config)?
} else {
SymphoniaCodec::open_native(demuxer.native_params())?
};
let pool = config
.pcm_pool
.clone()
.unwrap_or_else(|| PcmPool::default().clone());
let decoder = ComposedDecoder::new(
demuxer,
codec_impl,
pool,
config.epoch,
config.byte_len_handle.clone(),
config.hooks.clone(),
);
Ok(Box::new(decoder))
}
fn should_use_segment_aware(
codec: AudioCodec,
container: Option<ContainerFormat>,
config: &DecoderConfig,
) -> bool {
matches!(
codec,
AudioCodec::AacLc | AudioCodec::AacHe | AudioCodec::AacHeV2 | AudioCodec::Flac
) && matches!(container, Some(ContainerFormat::Fmp4))
&& config.segment_layout.is_some()
}
#[cfg(feature = "symphonia")]
fn create_fmp4_segment_symphonia(
source: BoxedSource,
codec: AudioCodec,
layout: Arc<dyn SegmentLayout>,
config: &DecoderConfig,
) -> DecodeResult<Box<dyn Decoder>> {
use crate::symphonia::{SymphoniaCodec, SymphoniaConfig};
tracing::debug!(
?codec,
"fmp4_segment: dispatching to segment-aware Symphonia path"
);
match codec {
AudioCodec::AacLc | AudioCodec::AacHe | AudioCodec::AacHeV2 | AudioCodec::Flac => {
let symphonia_config = SymphoniaConfig {
gapless: config.gapless,
..Default::default()
};
build_fmp4_segment_decoder(source, layout, config, |track| {
SymphoniaCodec::open_with_config(track, &symphonia_config)
})
}
other => Err(DecodeError::UnsupportedCodec(other)),
}
}
fn build_fmp4_segment_decoder<C, F>(
source: BoxedSource,
layout: Arc<dyn SegmentLayout>,
config: &DecoderConfig,
open_codec: F,
) -> DecodeResult<Box<dyn Decoder>>
where
C: crate::codec::FrameCodec + 'static,
F: FnOnce(&crate::demuxer::TrackInfo) -> DecodeResult<C>,
{
use crate::{composed::ComposedDecoder, demuxer::Demuxer, fmp4::Fmp4SegmentDemuxer};
let demuxer = Fmp4SegmentDemuxer::open(source, layout)?;
let codec = open_codec(demuxer.track_info())?;
let pool = config
.pcm_pool
.clone()
.unwrap_or_else(|| PcmPool::default().clone());
let decoder = ComposedDecoder::new(
demuxer,
codec,
pool,
config.epoch,
config.byte_len_handle.clone(),
config.hooks.clone(),
);
Ok(Box::new(decoder))
}